diff options
Diffstat (limited to 'server/sonar-web')
47 files changed, 1156 insertions, 5539 deletions
diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index 3d1e7aba4e6..5a7513de971 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -59,10 +59,7 @@ module.exports = ({ production = true, fast = false }) => ({ 'backbone', 'backbone.marionette', 'handlebars/runtime', - './src/main/js/libs/third-party/jquery-ui.js', - './src/main/js/libs/third-party/select2.js', - './src/main/js/libs/third-party/bootstrap/tooltip.js', - './src/main/js/libs/third-party/bootstrap/dropdown.js' + './src/main/js/libs/third-party/jquery-ui.js' ].filter(Boolean), app: [ diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 80f9ebb31de..3a78c92dc46 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -22,6 +22,7 @@ import { Link } from 'react-router'; import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; import { BranchLike, Component, Extension } from '../../../types'; +import Dropdown from '../../../../components/controls/Dropdown'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; import { isShortLivingBranch, @@ -182,17 +183,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } return ( - <li className="dropdown"> - <a - className={classNames('dropdown-toggle', { active: isSettingsActive })} - id="component-navigation-admin" - data-toggle="dropdown" - href="#"> - {translate('layout.settings')} - <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu">{adminLinks}</ul> - </li> + <Dropdown data-test="extensions"> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { active: isSettingsActive || open })} + href="#" + id="component-navigation-admin" + onClick={onToggleClick}> + {translate('layout.settings')} + <i className="icon-dropdown little-spacer-left" /> + </a> + <ul className="dropdown-menu">{adminLinks}</ul> + </li> + )} + </Dropdown> ); } @@ -416,17 +421,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } return ( - <li className="dropdown"> - <a - className="dropdown-toggle" - id="component-navigation-more" - data-toggle="dropdown" - href="#"> - {translate('more')} - <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu">{extensions.map(e => this.renderExtension(e, false))}</ul> - </li> + <Dropdown data-test="admin-extensions"> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { active: open })} + href="#" + id="component-navigation-more" + onClick={onToggleClick}> + {translate('more')} + <i className="icon-dropdown little-spacer-left" /> + </a> + <ul className="dropdown-menu">{extensions.map(e => this.renderExtension(e, false))}</ul> + </li> + )} + </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index ef502e33aad..4ac93c4fb00 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -38,11 +38,11 @@ it('should work with extensions', () => { configuration: { showSettings: true, extensions: [{ key: 'foo', name: 'Foo' }] }, extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }; - expect( - shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { - context: { branchesEnabled: true } - }) - ).toMatchSnapshot(); + const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { + context: { branchesEnabled: true } + }); + expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot(); }); it('should work with multiple extensions', () => { @@ -57,11 +57,11 @@ it('should work with multiple extensions', () => { { key: 'component-bar', name: 'ComponentBar' } ] }; - expect( - shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { - context: { branchesEnabled: true } - }) - ).toMatchSnapshot(); + const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { + context: { branchesEnabled: true } + }); + expect(wrapper.find('Dropdown[data-test="extensions"]').dive()).toMatchSnapshot(); + expect(wrapper.find('Dropdown[data-test="admin-extensions"]').dive()).toMatchSnapshot(); }); it('should work for short-living branches', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 5665e5ab9b4..249f20e49dc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -89,102 +89,9 @@ exports[`should work for all qualifiers 1`] = ` project_activity.page </Link> </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="settings" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, - } - } - > - project_settings.page - </Link> - </li> - <li - key="branches" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, - } - } - > - project_branches.page - </Link> - </li> - <li - key="webhooks" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, - } - } - > - webhooks.page - </Link> - </li> - <li - key="project_delete" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, - } - } - > - deletion.page - </Link> - </li> - </ul> - </li> + <Dropdown + data-test="extensions" + /> </NavBarTabs> `; @@ -277,45 +184,9 @@ exports[`should work for all qualifiers 2`] = ` project_activity.page </Link> </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="settings" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, - } - } - > - project_settings.page - </Link> - </li> - </ul> - </li> + <Dropdown + data-test="extensions" + /> </NavBarTabs> `; @@ -408,45 +279,9 @@ exports[`should work for all qualifiers 3`] = ` project_activity.page </Link> </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="project_delete" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, - } - } - > - deletion.page - </Link> - </li> - </ul> - </li> + <Dropdown + data-test="extensions" + /> </NavBarTabs> `; @@ -631,45 +466,9 @@ exports[`should work for all qualifiers 5`] = ` project_activity.page </Link> </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li - key="project_delete" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, - } - } - > - deletion.page - </Link> - </li> - </ul> - </li> + <Dropdown + data-test="extensions" + /> </NavBarTabs> `; @@ -911,531 +710,355 @@ exports[`should work for short-living branches 1`] = ` `; exports[`should work with extensions 1`] = ` -<NavBarTabs> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - overview.page - </Link> - </li> - <li> - <Link - activeClassName="active" - className="" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, - } - } - > - issues.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, - } - } - > - layout.measures - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/code", - "query": Object { - "id": "foo", - }, - } - } - > - code.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, - } - } - > - project_activity.page - </Link> - </li> - <li - className="dropdown" +<li + className="dropdown" +> + <a + className="dropdown-toggle" + href="#" + id="component-navigation-admin" + onClick={[Function]} > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" + layout.settings + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li + key="settings" > - <li - key="settings" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, - } + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, } - > - project_settings.page - </Link> - </li> - <li - key="branches" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, - } + project_settings.page + </Link> + </li> + <li + key="branches" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/branches", + "query": Object { + "id": "foo", + }, } - > - project_branches.page - </Link> - </li> - <li - key="webhooks" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, - } + project_branches.page + </Link> + </li> + <li + key="webhooks" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/webhooks", + "query": Object { + "id": "foo", + }, } - > - webhooks.page - </Link> - </li> - <li - key="foo" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - }, - } + webhooks.page + </Link> + </li> + <li + key="foo" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/foo", + "query": Object { + "id": "foo", + }, } - > - Foo - </Link> - </li> - <li - key="project_delete" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, - } + Foo + </Link> + </li> + <li + key="project_delete" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, } - > - deletion.page - </Link> - </li> - </ul> - </li> - <li - className="dropdown" + } + > + deletion.page + </Link> + </li> + </ul> +</li> +`; + +exports[`should work with extensions 2`] = ` +<li + className="dropdown" +> + <a + className="dropdown-toggle" + href="#" + id="component-navigation-more" + onClick={[Function]} > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-more" - > - more - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" + more + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li + key="component-foo" > - <li - key="component-foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - }, - } + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-foo", + "query": Object { + "id": "foo", + }, } - > - ComponentFoo - </Link> - </li> - </ul> - </li> -</NavBarTabs> + } + > + ComponentFoo + </Link> + </li> + </ul> +</li> `; exports[`should work with multiple extensions 1`] = ` -<NavBarTabs> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - overview.page - </Link> - </li> - <li> - <Link - activeClassName="active" - className="" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/issues", - "query": Object { - "id": "foo", - "resolved": "false", - }, - } - } - > - issues.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/component_measures", - "query": Object { - "id": "foo", - }, - } - } - > - layout.measures - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/code", - "query": Object { - "id": "foo", - }, - } - } - > - code.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/activity", - "query": Object { - "id": "foo", - }, - } - } - > - project_activity.page - </Link> - </li> - <li - className="dropdown" +<li + className="dropdown" +> + <a + className="dropdown-toggle" + href="#" + id="component-navigation-admin" + onClick={[Function]} > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-admin" - > - layout.settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" + layout.settings + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li + key="settings" > - <li - key="settings" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/settings", - "query": Object { - "id": "foo", - }, - } + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "id": "foo", + }, } - > - project_settings.page - </Link> - </li> - <li - key="branches" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/branches", - "query": Object { - "id": "foo", - }, - } + project_settings.page + </Link> + </li> + <li + key="branches" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/branches", + "query": Object { + "id": "foo", + }, } - > - project_branches.page - </Link> - </li> - <li - key="webhooks" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/webhooks", - "query": Object { - "id": "foo", - }, - } + project_branches.page + </Link> + </li> + <li + key="webhooks" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/webhooks", + "query": Object { + "id": "foo", + }, } - > - webhooks.page - </Link> - </li> - <li - key="foo" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/foo", - "query": Object { - "id": "foo", - }, - } + webhooks.page + </Link> + </li> + <li + key="foo" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/foo", + "query": Object { + "id": "foo", + }, } - > - Foo - </Link> - </li> - <li - key="bar" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/admin/extension/bar", - "query": Object { - "id": "foo", - }, - } + Foo + </Link> + </li> + <li + key="bar" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/admin/extension/bar", + "query": Object { + "id": "foo", + }, } - > - Bar - </Link> - </li> - <li - key="project_delete" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/deletion", - "query": Object { - "id": "foo", - }, - } + Bar + </Link> + </li> + <li + key="project_delete" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/deletion", + "query": Object { + "id": "foo", + }, } - > - deletion.page - </Link> - </li> - </ul> - </li> - <li - className="dropdown" + } + > + deletion.page + </Link> + </li> + </ul> +</li> +`; + +exports[`should work with multiple extensions 2`] = ` +<li + className="dropdown" +> + <a + className="dropdown-toggle" + href="#" + id="component-navigation-more" + onClick={[Function]} > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="component-navigation-more" - > - more - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" + more + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li + key="component-foo" > - <li - key="component-foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-foo", - "query": Object { - "id": "foo", - }, - } + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-foo", + "query": Object { + "id": "foo", + }, } - > - ComponentFoo - </Link> - </li> - <li - key="component-bar" + } > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/project/extension/component-bar", - "query": Object { - "id": "foo", - }, - } + ComponentFoo + </Link> + </li> + <li + key="component-bar" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/extension/component-bar", + "query": Object { + "id": "foo", + }, } - > - ComponentBar - </Link> - </li> - </ul> - </li> -</NavBarTabs> + } + > + ComponentBar + </Link> + </li> + </ul> +</li> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index dacf97dc27f..443f074225c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -19,10 +19,12 @@ */ import * as React from 'react'; import { Link } from 'react-router'; +import * as classNames from 'classnames'; import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../../app/types'; import { translate } from '../../../../helpers/l10n'; import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls'; import { isMySet } from '../../../../apps/issues/utils'; +import Dropdown from '../../../../components/controls/Dropdown'; interface Props { appState: AppState; @@ -151,13 +153,21 @@ export default class GlobalNavMenu extends React.PureComponent<Props> { return null; } return ( - <li className="dropdown"> - <a className="dropdown-toggle" id="global-navigation-more" data-toggle="dropdown" href="#"> - {translate('more')} - <span className="icon-dropdown" /> - </a> - <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul> - </li> + <Dropdown> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { active: open })} + href="#" + id="global-navigation-more" + onClick={onToggleClick}> + {translate('more')} + <span className="icon-dropdown little-spacer-left" /> + </a> + <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul> + </li> + )} + </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 5f3a4d7e656..22fa861f34f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -32,7 +32,7 @@ it('should work with extensions', () => { const wrapper = shallow( <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} /> ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); }); it('should show administration menu if the user has the rights', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap index 34dd8f91396..27714386573 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap @@ -77,98 +77,34 @@ exports[`should show administration menu if the user has the rights 1`] = ` `; exports[`should work with extensions 1`] = ` -<ul - className="global-navbar-menu pull-left" +<li + className="dropdown" > - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/projects" - > - projects.page - </Link> - </li> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/issues", - "query": Object { - "resolved": "false", - }, - } - } - > - issues.page - </Link> - </li> - <li> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/coding_rules" - > - coding_rules.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/profiles" - > - quality_profiles.page - </Link> - </li> - <li> - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/quality_gates", - } - } - > - quality_gates.page - </Link> - </li> - <li - className="dropdown" + <a + className="dropdown-toggle" + href="#" + id="global-navigation-more" + onClick={[Function]} > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - id="global-navigation-more" - > - more - - <span - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" + more + <span + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li + key="foo" > - <li - key="foo" + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/extension/foo" > - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/extension/foo" - > - Foo - </Link> - </li> - </ul> - </li> -</ul> + Foo + </Link> + </li> + </ul> +</li> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 688bce71d8e..3064c54212e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -27,6 +27,7 @@ import NavBarTabs from '../../../../components/nav/NavBarTabs'; import { EditionStatus } from '../../../../api/marketplace'; import { Extension } from '../../../types'; import { translate } from '../../../../helpers/l10n'; +import Dropdown from '../../../../components/controls/Dropdown'; interface Props { editionStatus?: EditionStatus; @@ -82,118 +83,135 @@ export default class SettingsNav extends React.PureComponent<Props> { renderConfigurationTab() { const { organizationsEnabled } = this.props; - const configurationClassNames = classNames('dropdown-toggle', { - active: - !this.isSecurityActive() && - !this.isProjectsActive() && - !this.isSystemActive() && - !this.isSomethingActive(['/admin/extension/license/support']) && - !this.isMarketplace() - }); const extensionsWithoutSupport = this.props.extensions.filter( extension => extension.key !== 'license/support' ); return ( - <li className="dropdown"> - <a - className={configurationClassNames} - data-toggle="dropdown" - href="#" - id="settings-navigation-configuration"> - {translate('sidebar.project_settings')} <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu"> - <li> - <IndexLink activeClassName="active" to="/admin/settings"> - {translate('settings.page')} - </IndexLink> - </li> - <li> - <IndexLink activeClassName="active" to="/admin/settings/encryption"> - {translate('property.category.security.encryption')} - </IndexLink> + <Dropdown> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { + active: + open || + (!this.isSecurityActive() && + !this.isProjectsActive() && + !this.isSystemActive() && + !this.isSomethingActive(['/admin/extension/license/support']) && + !this.isMarketplace()) + })} + href="#" + id="settings-navigation-configuration" + onClick={onToggleClick}> + {translate('sidebar.project_settings')} + <i className="icon-dropdown little-spacer-left" /> + </a> + <ul className="dropdown-menu"> + <li> + <IndexLink activeClassName="active" to="/admin/settings"> + {translate('settings.page')} + </IndexLink> + </li> + <li> + <IndexLink activeClassName="active" to="/admin/settings/encryption"> + {translate('property.category.security.encryption')} + </IndexLink> + </li> + <li> + <IndexLink activeClassName="active" to="/admin/custom_metrics"> + {translate('custom_metrics.page')} + </IndexLink> + </li> + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/webhooks"> + {translate('webhooks.page')} + </IndexLink> + </li> + )} + {extensionsWithoutSupport.map(this.renderExtension)} + </ul> </li> - <li> - <IndexLink activeClassName="active" to="/admin/custom_metrics"> - {translate('custom_metrics.page')} - </IndexLink> - </li> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/webhooks"> - {translate('webhooks.page')} - </IndexLink> - </li> - )} - {extensionsWithoutSupport.map(this.renderExtension)} - </ul> - </li> + )} + </Dropdown> ); } renderProjectsTab() { const { organizationsEnabled } = this.props; - const projectsClassName = classNames('dropdown-toggle', { active: this.isProjectsActive() }); return ( - <li className="dropdown"> - <a className={projectsClassName} data-toggle="dropdown" href="#"> - {translate('sidebar.projects')} <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu"> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/projects_management"> - {translate('management')} - </IndexLink> - </li> - )} - <li> - <IndexLink activeClassName="active" to="/admin/background_tasks"> - {translate('background_tasks.page')} - </IndexLink> + <Dropdown> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { active: open || this.isProjectsActive() })} + href="#" + onClick={onToggleClick}> + {translate('sidebar.projects')} <i className="icon-dropdown" /> + </a> + <ul className="dropdown-menu"> + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/projects_management"> + {translate('management')} + </IndexLink> + </li> + )} + <li> + <IndexLink activeClassName="active" to="/admin/background_tasks"> + {translate('background_tasks.page')} + </IndexLink> + </li> + </ul> </li> - </ul> - </li> + )} + </Dropdown> ); } renderSecurityTab() { const { organizationsEnabled } = this.props; - const securityClassName = classNames('dropdown-toggle', { active: this.isSecurityActive() }); return ( - <li className="dropdown"> - <a className={securityClassName} data-toggle="dropdown" href="#"> - {translate('sidebar.security')} <i className="icon-dropdown" /> - </a> - <ul className="dropdown-menu"> - <li> - <IndexLink activeClassName="active" to="/admin/users"> - {translate('users.page')} - </IndexLink> + <Dropdown> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', { open })}> + <a + className={classNames('dropdown-toggle', { active: open || this.isSecurityActive() })} + href="#" + onClick={onToggleClick}> + {translate('sidebar.security')} <i className="icon-dropdown" /> + </a> + <ul className="dropdown-menu"> + <li> + <IndexLink activeClassName="active" to="/admin/users"> + {translate('users.page')} + </IndexLink> + </li> + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/groups"> + {translate('user_groups.page')} + </IndexLink> + </li> + )} + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/permissions"> + {translate('global_permissions.page')} + </IndexLink> + </li> + )} + {!organizationsEnabled && ( + <li> + <IndexLink activeClassName="active" to="/admin/permission_templates"> + {translate('permission_templates')} + </IndexLink> + </li> + )} + </ul> </li> - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/groups"> - {translate('user_groups.page')} - </IndexLink> - </li> - )} - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/permissions"> - {translate('global_permissions.page')} - </IndexLink> - </li> - )} - {!organizationsEnabled && ( - <li> - <IndexLink activeClassName="active" to="/admin/permission_templates"> - {translate('permission_templates')} - </IndexLink> - </li> - )} - </ul> - </li> + )} + </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index 6deee4fd798..e589b879494 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -27,4 +27,5 @@ it('should work with extensions', () => { <SettingsNav extensions={extensions} location={{}} organizationsEnabled={false} /> ); expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').map(x => x.dive())).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index a80573d7d9d..439e362124e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -13,156 +13,9 @@ exports[`should work with extensions 1`] = ` </h1> </header> <NavBarTabs> - <li - className="dropdown" - > - <a - className="dropdown-toggle active" - data-toggle="dropdown" - href="#" - id="settings-navigation-configuration" - > - sidebar.project_settings - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/settings" - > - settings.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/settings/encryption" - > - property.category.security.encryption - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/custom_metrics" - > - custom_metrics.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/webhooks" - > - webhooks.page - </IndexLink> - </li> - <li - key="foo" - > - <Link - activeClassName="active" - onlyActiveOnIndex={false} - style={Object {}} - to="/admin/extension/foo" - > - Foo - </Link> - </li> - </ul> - </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - > - sidebar.security - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/users" - > - users.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/groups" - > - user_groups.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/permissions" - > - global_permissions.page - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/permission_templates" - > - permission_templates - </IndexLink> - </li> - </ul> - </li> - <li - className="dropdown" - > - <a - className="dropdown-toggle" - data-toggle="dropdown" - href="#" - > - sidebar.projects - - <i - className="icon-dropdown" - /> - </a> - <ul - className="dropdown-menu" - > - <li> - <IndexLink - activeClassName="active" - to="/admin/projects_management" - > - management - </IndexLink> - </li> - <li> - <IndexLink - activeClassName="active" - to="/admin/background_tasks" - > - background_tasks.page - </IndexLink> - </li> - </ul> - </li> + <Dropdown /> + <Dropdown /> + <Dropdown /> <li> <IndexLink activeClassName="active" @@ -182,3 +35,157 @@ exports[`should work with extensions 1`] = ` </NavBarTabs> </ContextNavBar> `; + +exports[`should work with extensions 2`] = ` +Array [ + <li + className="dropdown" + > + <a + className="dropdown-toggle active" + href="#" + id="settings-navigation-configuration" + onClick={[Function]} + > + sidebar.project_settings + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <ul + className="dropdown-menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/settings" + > + settings.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/settings/encryption" + > + property.category.security.encryption + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/custom_metrics" + > + custom_metrics.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/webhooks" + > + webhooks.page + </IndexLink> + </li> + <li + key="foo" + > + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to="/admin/extension/foo" + > + Foo + </Link> + </li> + </ul> + </li>, + <li + className="dropdown" + > + <a + className="dropdown-toggle" + href="#" + onClick={[Function]} + > + sidebar.security + + <i + className="icon-dropdown" + /> + </a> + <ul + className="dropdown-menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/users" + > + users.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/groups" + > + user_groups.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permissions" + > + global_permissions.page + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/permission_templates" + > + permission_templates + </IndexLink> + </li> + </ul> + </li>, + <li + className="dropdown" + > + <a + className="dropdown-toggle" + href="#" + onClick={[Function]} + > + sidebar.projects + + <i + className="icon-dropdown" + /> + </a> + <ul + className="dropdown-menu" + > + <li> + <IndexLink + activeClassName="active" + to="/admin/projects_management" + > + management + </IndexLink> + </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/background_tasks" + > + background_tasks.page + </IndexLink> + </li> + </ul> + </li>, +] +`; diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css index dbe8a64cb98..21d73c39a99 100644 --- a/server/sonar-web/src/main/js/app/styles/components/modals.css +++ b/server/sonar-web/src/main/js/app/styles/components/modals.css @@ -113,10 +113,6 @@ ul.modal-head-metadata li { padding: 10px; } -.modal-body-select2 { - margin-bottom: 10px; -} - .modal-body .notes { height: auto; } diff --git a/server/sonar-web/src/main/js/app/styles/select2-sonar.css b/server/sonar-web/src/main/js/app/styles/select2-sonar.css deleted file mode 100644 index 5987cc3da25..00000000000 --- a/server/sonar-web/src/main/js/app/styles/select2-sonar.css +++ /dev/null @@ -1,192 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -.select2-container { - vertical-align: middle; -} - -.select2-container .select2-choice { - height: var(--controlHeight); - line-height: 22px; - border-color: var(--gray80); - border-radius: 2px; - box-sizing: border-box; - background: #fff; - font-size: var(--smallFontSize); - text-align: left; -} - -.select2-container .select2-choice, -.select2-container .select2-choices { - transition: border-color 0.2s ease; -} - -.select2-container .select2-choice abbr { - top: 4px; -} - -.select2-container .select2-choice div { - width: 19px; - border: none; - border-radius: 0; - background: #fff; -} - -.select2-container .select2-choice div b { - top: 4px; - background-position: 1px -1px; -} - -.select2-dropdown-open .select2-choice div b { - background-position: -17px -1px; -} - -.select2-container .select2-choice span i { - position: relative; - top: 2px; -} - -.select2-container-active .select2-choice, -.select2-container-active .select2-choices { - border-color: var(--blue); - box-shadow: none; -} - -.select2-dropdown-open .select2-choice { - box-shadow: none; -} - -.select2-drop { - z-index: var(--dropdownMenuZIndex); - border-color: var(--gray80); - border-radius: 0; -} - -.select2-drop-active { - border-color: var(--blue); -} - -.select2-dropdown-open.select2-drop-above .select2-choice, -.select2-dropdown-open.select2-drop-above .select2-choices { - border-color: var(--blue); - border-radius: 0; - background: #fff; -} - -.select2-drop.select2-drop-above.select2-drop-active { - border-color: var(--blue); - border-radius: 0; -} - -.select2-drop.select2-drop-above .select2-search input { - margin-top: 0; -} - -.select2-results { - margin: 0; - padding: 5px 0; - border-top: 1px solid var(--gray80); -} - -.select2-results .select2-result-label { - height: 20px; - line-height: 20px; - padding: 0 8px; - color: var(--baseFontColor); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.select2-results .select2-no-results, -.select2-results .select2-searching, -.select2-results .select2-selection-limit, -.select2-more-results.select2-active { - height: 20px; - line-height: 20px; - padding: 0 10px; -} - -.select2-results .select2-highlighted { - background: transparent; - color: var(--baseFontColor); -} - -.select2-results .select2-highlighted .select2-result-label { - background: #e2e2e2; -} - -.select2-search { - padding: 4px; -} - -.select2-search input { - height: 20px; - padding: 0 7px; - border-color: var(--gray80); - background: #fff !important; -} - -.select2-container-multi .select2-choices { - min-height: 19px; - padding-bottom: 1px; - border-color: var(--gray80); - background: #fff; -} - -.select2-container-multi.select2-container-active .select2-choices { - border-color: var(--blue); - box-shadow: none; -} - -.select2-container-multi .select2-choices .select2-search-field input { - height: 16px; - padding: 0 3px; -} - -.select2-container-multi .select2-choices .select2-search-choice { - margin: 1px 1px 0 1px; - padding: 1px 5px 2px 18px; - border-radius: 0; - border-color: var(--gray80); - background: var(--gray94); - box-shadow: none; -} - -.select2-search-choice-close { - top: 2px; -} - -.select2-search-choice-close, -.select2-container .select2-choice abbr, -.select2-container .select2-choice div b { - background-image: url('../images/select2x2.png'); - background-size: 60px 40px; -} - -.select2-search input.select2-active, -.select2-more-results.select2-active, -.select2-container-multi .select2-choices .select2-search-field input.select2-active { - background-image: url('../images/loading.gif'); -} - -.select2-offscreen { - left: 0; - top: -100000px; -} diff --git a/server/sonar-web/src/main/js/app/styles/select2.css b/server/sonar-web/src/main/js/app/styles/select2.css deleted file mode 100644 index f24b98de573..00000000000 --- a/server/sonar-web/src/main/js/app/styles/select2.css +++ /dev/null @@ -1,493 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -/* -Version: 3.2 Timestamp: Mon Sep 10 10:38:04 PDT 2012 -*/ -.select2-container { - position: relative; - display: inline-block; - vertical-align: top; -} - -.select2-container, -.select2-drop, -.select2-search, -.select2-search input { - box-sizing: border-box; -} - -.select2-container .select2-choice { - background: #fff linear-gradient(to bottom, #eee 0%, #fff 50%); - border-radius: 4px; - background-clip: padding-box; - border: 1px solid #aaa; - display: block; - overflow: hidden; - white-space: nowrap; - position: relative; - height: 26px; - line-height: 26px; - padding: 0 0 0 8px; - color: var(--baseFontColor); - text-decoration: none; -} - -.select2-container.select2-drop-above .select2-choice { - border-bottom-color: #aaa; - border-radius: 0 0 4px 4px; - background-image: linear-gradient(to bottom, #eeeeee 0%, #ffffff 90%); -} - -.select2-container .select2-choice span { - margin-right: 26px; - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.select2-container .select2-choice abbr { - display: block; - position: absolute; - right: 26px; - top: 8px; - width: 12px; - height: 12px; - font-size: 1px; - background: url('../images/select2.png') right top no-repeat; - cursor: pointer; - text-decoration: none; - border: 0; - outline: 0; -} - -.select2-container .select2-choice abbr:hover { - background-position: right -11px; - cursor: pointer; -} - -.select2-drop { - background: #fff; - color: #000; - border: 1px solid #aaa; - border-top: 0; - position: absolute; - top: 100%; - box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); - z-index: 9999; - width: 100%; - margin-top: -1px; - border-radius: 0 0 4px 4px; -} - -.select2-drop.select2-drop-above { - border-radius: 4px 4px 0 0; - margin-top: 1px; - border-top: 1px solid #aaa; - border-bottom: 0; - box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15); -} - -.select2-container .select2-choice div { - border-radius: 0 4px 4px 0; - background-clip: padding-box; - background: #ccc linear-gradient(to bottom, #ccc 0%, #eee 60%); - border-left: 1px solid #aaa; - position: absolute; - right: 0; - top: 0; - display: block; - height: 100%; - width: 18px; -} - -.select2-container .select2-choice div b { - background: url('../images/select2.png') no-repeat 0 1px; - display: block; - width: 100%; - height: 100%; -} - -.select2-search { - display: inline-block; - white-space: nowrap; - z-index: 10000; - min-height: 26px; - width: 100%; - margin: 0; - padding-left: 4px; - padding-right: 4px; -} - -.select2-search-hidden { - display: block; - position: absolute; - left: -10000px; -} - -.select2-search input { - background: #fff url('../images/select2.png') no-repeat 100% -22px, - linear-gradient(to bottom, #fff 85%, #eee 99%); - padding: 4px 20px 4px 5px; - outline: 0; - border: 1px solid #aaa; - font-family: sans-serif; - font-size: 1em; - width: 100%; - margin: 0; - height: auto !important; - min-height: 26px; - box-shadow: none; - border-radius: 0; -} - -.select2-drop.select2-drop-above .select2-search input { - margin-top: 4px; -} - -.select2-search input.select2-active { - background: #ffff f url('../images/loading.gif') no-repeat 100%, - linear-gradient(to bottom, #fff 85%, #eee 99%); -} - -.select2-container-active .select2-choice, -.select2-container-active .select2-choices { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - border: 1px solid #5897fb; - outline: none; -} - -.select2-dropdown-open .select2-choice { - border: 1px solid #aaa; - border-bottom-color: transparent; - box-shadow: 0 1px 0 #fff inset; - background: #eee linear-gradient(to bottom, #fff 0%, #eee 50%); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.select2-dropdown-open .select2-choice div { - background: transparent; - border-left: none; -} - -.select2-dropdown-open .select2-choice div b { - background-position: -18px 1px; -} - -/* results */ -.select2-results { - margin: 4px 4px 4px 0; - padding: 0 0 0 4px; - position: relative; - overflow-x: hidden; - overflow-y: auto; - max-height: 200px; -} - -.select2-results ul.select2-result-sub { - margin: 0 0 0 0; -} - -.select2-results ul.select2-result-sub > li .select2-result-label { - padding-left: 20px; -} - -.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { - padding-left: 40px; -} - -.select2-results - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - > li - .select2-result-label { - padding-left: 60px; -} - -.select2-results - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - > li - .select2-result-label { - padding-left: 80px; -} - -.select2-results - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - > li - .select2-result-label { - padding-left: 100px; -} - -.select2-results - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - > li - .select2-result-label { - padding-left: 110px; -} - -.select2-results - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - ul.select2-result-sub - > li - .select2-result-label { - padding-left: 120px; -} - -.select2-results li { - list-style: none; - display: list-item; -} - -.select2-results li.select2-result-with-children > .select2-result-label { - font-weight: bold; -} - -.select2-results .select2-result-label { - padding: 3px 7px 4px; - margin: 0; - cursor: pointer; -} - -.select2-results .select2-highlighted { - background: #3875d7; - color: #fff; -} - -.select2-results li em { - background: #feffde; - font-style: normal; -} - -.select2-results .select2-highlighted em { - background: transparent; -} - -.select2-results .select2-no-results, -.select2-results .select2-searching, -.select2-results .select2-selection-limit { - background: #f4f4f4; - display: list-item; -} - -.select2-results .select2-disabled { - display: none; -} - -.select2-more-results.select2-active { - background: #f4f4f4 url('../images/loading.gif') no-repeat 100%; -} - -.select2-more-results { - background: #f4f4f4; - display: list-item; -} - -/* disabled styles */ -.select2-container.select2-container-disabled .select2-choice { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; -} - -.select2-container.select2-container-disabled .select2-choice div { - background-color: #f4f4f4; - background-image: none; - border-left: 0; -} - -/* multiselect */ -.select2-container-multi .select2-choices { - background: #fff linear-gradient(to bottom, #eee 1%, #fff 15%); - border: 1px solid #aaa; - margin: 0; - padding: 0; - cursor: text; - overflow: hidden; - height: auto !important; - height: 1%; - position: relative; -} - -.select2-container-multi .select2-choices { - min-height: 26px; -} - -.select2-container-multi.select2-container-active .select2-choices { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - border: 1px solid #5897fb; - outline: none; -} - -.select2-container-multi .select2-choices li { - float: left; - list-style: none; -} - -.select2-container-multi .select2-choices .select2-search-field { - white-space: nowrap; - margin: 0; - padding: 0; -} - -.select2-container-multi .select2-choices .select2-search-field input { - color: #666; - background: transparent !important; - font-family: sans-serif; - font-size: 100%; - height: 15px; - padding: 5px; - margin: 1px 0; - outline: 0; - border: 0; - box-shadow: none; -} - -.select2-container-multi .select2-choices .select2-search-field input.select2-active { - background: #fff url('../images/loading.gif') no-repeat 100% !important; -} - -.select2-default { - color: #999 !important; -} - -.select2-container-multi .select2-choices .select2-search-choice { - border-radius: 3px; - background-clip: padding-box; - background: #e4e4e4 linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); - box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05); - color: #333; - border: 1px solid #aaaaaa; - line-height: var(--baseFontSize); - padding: 3px 5px 3px 18px; - margin: 3px 0 3px 5px; - position: relative; - cursor: default; -} - -.select2-container-multi .select2-choices .select2-search-choice span { - cursor: default; -} - -.select2-container-multi .select2-choices .select2-search-choice-focus { - background: #d4d4d4; -} - -.select2-search-choice-close { - display: block; - position: absolute; - right: 3px; - top: 4px; - width: 12px; - height: 13px; - font-size: 1px; - background: url('../images/select2.png') right top no-repeat; - outline: none; -} - -.select2-container-multi .select2-search-choice-close { - left: 3px; -} - -.select2-container-multi - .select2-choices - .select2-search-choice - .select2-search-choice-close:hover { - background-position: right -11px; -} - -.select2-container-multi - .select2-choices - .select2-search-choice-focus - .select2-search-choice-close { - background-position: right -11px; -} - -/* disabled styles */ -.select2-container-multi.select2-container-disabled .select2-choices { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; -} - -.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { - background-image: none; - background-color: #f4f4f4; - border: 1px solid #ddd; - padding: 3px 5px 3px 5px; -} - -.select2-container-multi.select2-container-disabled - .select2-choices - .select2-search-choice - .select2-search-choice-close { - display: none; -} - -/* end multiselect */ -.select2-result-selectable .select2-match, -.select2-result-unselectable .select2-result-selectable .select2-match { - text-decoration: underline; -} - -.select2-result-unselectable .select2-match { - text-decoration: none; -} - -.select2-offscreen { - position: absolute; - left: -10000px; -} - -/* Retina-ize icons */ -@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - .select2-search input, - .select2-search-choice-close, - .select2-container .select2-choice abbr, - .select2-container .select2-choice div b { - background-image: url(../images/select2x2.png) !important; - background-repeat: no-repeat !important; - background-size: 60px 40px !important; - } - - .select2-search input { - background-position: 100% -21px !important; - } -} diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css index eda7ed09c44..f48136b4f1f 100644 --- a/server/sonar-web/src/main/js/app/styles/sonar.css +++ b/server/sonar-web/src/main/js/app/styles/sonar.css @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @import './jquery-ui.css'; -@import './select2.css'; -@import './select2-sonar.css'; @import './init/base.css'; @import './init/type.css'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/styles.css b/server/sonar-web/src/main/js/apps/coding-rules/styles.css index 5647de8b17c..b553e505507 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/styles.css +++ b/server/sonar-web/src/main/js/apps/coding-rules/styles.css @@ -85,10 +85,6 @@ font-size: var(--smallFontSize); } -.coding-rules-detail-property .select2-search-field { - line-height: 1; -} - .coding-rules-detail-tag + .coding-rules-detail-tag { margin-left: 10px; } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js index 1998460996e..54d1e975031 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -95,10 +95,6 @@ export default class MeasureContentContainer extends React.PureComponent { const metricKeys = [metric.key]; if (metric.key === 'ncloc') { metricKeys.push('ncloc_language_distribution'); - } else if (metric.key === 'function_complexity') { - metricKeys.push('function_complexity_distribution'); - } else if (metric.key === 'file_complexity') { - metricKeys.push('file_complexity_distribution'); } fetchMeasures(selected || rootComponent.key, metricKeys, branchLike).then( diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js index 5c809374492..abff84b4d2c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -21,7 +21,6 @@ import React from 'react'; import { Link } from 'react-router'; import LeakPeriodLegend from './LeakPeriodLegend'; -import ComplexityDistribution from '../../../components/shared/ComplexityDistribution'; import HistoryIcon from '../../../components/icons-components/HistoryIcon'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer'; @@ -94,18 +93,6 @@ export default function MeasureHeader(props /*: Props*/) { /> </div> )} - {secondaryMeasure && - secondaryMeasure.metric.key === 'function_complexity_distribution' && ( - <div className="measure-details-secondary"> - <ComplexityDistribution distribution={secondaryMeasure.value} of="function" /> - </div> - )} - {secondaryMeasure && - secondaryMeasure.metric.key === 'file_complexity_distribution' && ( - <div className="measure-details-secondary"> - <ComplexityDistribution distribution={secondaryMeasure.value} of="file" /> - </div> - )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js index 74fb563743e..4c6ae9e9b4e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js @@ -84,7 +84,16 @@ export default class BubbleChart extends React.PureComponent { } }); } - return `<div class="text-left">${inner.join('<br/>')}</div>`; + return ( + <div className="text-left"> + {inner.map((line, index) => ( + <React.Fragment key={index}> + {line} + {index < inner.length - 1 && <br />} + </React.Fragment> + ))} + </div> + ); } handleBubbleClick = (component /*: ComponentEnhanced */) => diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 85bf0fa458e..1eb850bc266 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -23,6 +23,7 @@ import Helmet from 'react-helmet'; import key from 'keymaster'; import { keyBy, union, without } from 'lodash'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import PageActions from './PageActions'; import MyIssuesFilter from './MyIssuesFilter'; import IssuesList from './IssuesList'; @@ -55,10 +56,12 @@ import { CurrentUser } from '../utils'; */ import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; +import Dropdown from '../../../components/controls/Dropdown'; import ListFooter from '../../../components/controls/ListFooter'; import EmptySearch from '../../../components/common/EmptySearch'; import FiltersHeader from '../../../components/common/FiltersHeader'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; +import { Button } from '../../../components/ui/buttons'; import { isShortLivingBranch, isSameBranchLike, @@ -710,9 +713,7 @@ export default class App extends React.PureComponent { this.setState({ bulkChange: null }); }; - handleBulkChangeClick = (e /*: Event & { target: HTMLElement } */) => { - e.preventDefault(); - e.target.blur(); + handleBulkChangeClick = () => { this.openBulkChange('all'); }; @@ -779,28 +780,32 @@ export default class App extends React.PureComponent { thirdState={thirdState} /> {checked.length > 0 ? ( - <div className="dropdown display-inline-block"> - <button id="issues-bulk-change" data-toggle="dropdown"> - {translate('bulk_change')} - <i className="icon-dropdown little-spacer-left" /> - </button> - <ul className="dropdown-menu"> - <li> - <a href="#" onClick={this.handleBulkChangeClick}> - {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} - </a> - </li> - <li> - <a href="#" onClick={this.handleBulkChangeSelectedClick}> - {translateWithParameters('issues.bulk_change_selected', checked.length)} - </a> - </li> - </ul> - </div> + <Dropdown> + {({ onToggleClick, open }) => ( + <div className={classNames('dropdown display-inline-block', { open })}> + <Button id="issues-bulk-change" onClick={onToggleClick}> + {translate('bulk_change')} + <i className="icon-dropdown little-spacer-left" /> + </Button> + <ul className="dropdown-menu"> + <li> + <a href="#" onClick={this.handleBulkChangeClick}> + {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)} + </a> + </li> + <li> + <a href="#" onClick={this.handleBulkChangeSelectedClick}> + {translateWithParameters('issues.bulk_change_selected', checked.length)} + </a> + </li> + </ul> + </div> + )} + </Dropdown> ) : ( - <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}> + <Button id="issues-bulk-change" onClick={this.handleBulkChangeClick}> {translate('bulk_change')} - </button> + </Button> )} {bulkChange != null && ( <BulkChangeModal diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js index e4ecafbf742..ed43efee2e3 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js @@ -171,14 +171,16 @@ export default class CreationDateFacet extends React.PureComponent { endDate = createdBefore && parseDate(createdBefore); } - let tooltip = - formatMeasure(stats[start], 'SHORT_INT') + - '<br/>' + - formatDate(startDate, longFormatterOption); const tooltipEndDate = endDate || Date.now(); - if (!isSameDay(tooltipEndDate, startDate)) { - tooltip += ' – ' + formatDate(tooltipEndDate, longFormatterOption); - } + const tooltip = ( + <React.Fragment> + {formatMeasure(stats[start], 'SHORT_INT')} + <br /> + {formatDate(startDate, longFormatterOption)} + {!isSameDay(tooltipEndDate, startDate) && + ` - ${formatDate(tooltipEndDate, longFormatterOption)}`} + </React.Fragment> + ); return { createdAfter: startDate, diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx index 13ae5e5b9b0..d7c004af86c 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx @@ -45,7 +45,13 @@ export default class Risk extends React.PureComponent<Props> { getMetricTooltip(metric: { key: string; type: string }, value?: number) { const name = translate('metric', metric.key, 'name'); const formattedValue = value != null ? formatMeasure(value, metric.type) : '–'; - return `<div>${name}: ${formattedValue}</div>`; + return ( + <div> + {name} + {': '} + {formattedValue} + </div> + ); } getTooltip( @@ -57,18 +63,26 @@ export default class Risk extends React.PureComponent<Props> { color2?: number ) { const fullProjectName = - this.props.displayOrganizations && project.organization - ? `${project.organization.name} / <strong>${project.name}</strong>` - : `<strong>${project.name}</strong>`; - const inner = [ - `<div class="little-spacer-bottom">${fullProjectName}</div>`, - this.getMetricTooltip({ key: COLOR_METRIC_1, type: COLOR_METRIC_TYPE }, color1), - this.getMetricTooltip({ key: COLOR_METRIC_2, type: COLOR_METRIC_TYPE }, color2), - this.getMetricTooltip({ key: Y_METRIC, type: Y_METRIC_TYPE }, y), - this.getMetricTooltip({ key: X_METRIC, type: X_METRIC_TYPE }, x), - this.getMetricTooltip({ key: SIZE_METRIC, type: SIZE_METRIC_TYPE }, size) - ].join(''); - return `<div class="text-left">${inner}</div>`; + this.props.displayOrganizations && project.organization ? ( + <> + {project.organization.name} + {' / '} + <strong>{project.name}</strong> + </> + ) : ( + <strong>{project.name}</strong> + ); + + return ( + <div className="text-left"> + <div className="little-spacer-bottom">{fullProjectName}</div> + {this.getMetricTooltip({ key: COLOR_METRIC_1, type: COLOR_METRIC_TYPE }, color1)} + {this.getMetricTooltip({ key: COLOR_METRIC_2, type: COLOR_METRIC_TYPE }, color2)} + {this.getMetricTooltip({ key: Y_METRIC, type: Y_METRIC_TYPE }, y)} + {this.getMetricTooltip({ key: X_METRIC, type: X_METRIC_TYPE }, x)} + {this.getMetricTooltip({ key: SIZE_METRIC, type: SIZE_METRIC_TYPE }, size)} + </div> + ); } render() { diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx index 28af230caf5..9b9d8af06aa 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx @@ -45,28 +45,37 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> { getMetricTooltip(metric: Metric, value?: number) { const name = translate('metric', metric.key, 'name'); const formattedValue = value != null ? formatMeasure(value, metric.type) : '–'; - return `<div>${name}: ${formattedValue}</div>`; + return ( + <div> + {name} + {': '} + {formattedValue} + </div> + ); } getTooltip(project: Project, x?: number, y?: number, size?: number, color?: number) { const fullProjectName = - this.props.displayOrganizations && project.organization - ? `${project.organization.name} / <strong>${project.name}</strong>` - : `<strong>${project.name}</strong>`; - - const inner = [ - `<div class="little-spacer-bottom">${fullProjectName}</div>`, - this.getMetricTooltip(this.props.xMetric, x), - this.getMetricTooltip(this.props.yMetric, y), - this.getMetricTooltip(this.props.sizeMetric, size) - ]; + this.props.displayOrganizations && project.organization ? ( + <> + {project.organization.name} + {' / '} + <strong>{project.name}</strong> + </> + ) : ( + <strong>{project.name}</strong> + ); - if (color) { - // if `color` is defined then `this.props.colorMetric` is defined too - this.getMetricTooltip({ key: this.props.colorMetric!, type: 'RATING' }, color); - } - - return `<div class="text-left">${inner.join('')}</div>`; + return ( + <div className="text-left"> + <div className="little-spacer-bottom">{fullProjectName}</div> + {this.getMetricTooltip(this.props.xMetric, x)} + {this.getMetricTooltip(this.props.yMetric, y)} + {this.getMetricTooltip(this.props.sizeMetric, size)} + {/* if `color` is defined then `this.props.colorMetric` is defined too */} + {color && this.getMetricTooltip({ key: this.props.colorMetric!, type: 'RATING' }, color)} + </div> + ); } render() { diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx index 3a19728ba66..e76c73076f0 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx @@ -24,7 +24,8 @@ import SimpleBubbleChart from '../SimpleBubbleChart'; it('renders', () => { const project1 = { key: 'foo', - measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' }, + // eslint-disable-next-line camelcase + measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' }, name: 'Foo', tags: [], visibility: 'public' diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap index 733d95af43f..ecc8cc1fede 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap @@ -22,7 +22,42 @@ exports[`renders 1`] = ` }, }, "size": 1734, - "tooltip": "<div class=\\"text-left\\"><div class=\\"little-spacer-bottom\\"><strong>Foo</strong></div><div>metric.reliability_rating.name: –</div><div>metric.security_rating.name: –</div><div>metric.coverage.name: 53.5%</div><div>metric.sqale_index.name: –</div><div>metric.ncloc.name: 1.7short_number_suffix.k</div></div>", + "tooltip": <div + className="text-left" + > + <div + className="little-spacer-bottom" + > + <strong> + Foo + </strong> + </div> + <div> + metric.reliability_rating.name + : + – + </div> + <div> + metric.security_rating.name + : + – + </div> + <div> + metric.coverage.name + : + 53.5% + </div> + <div> + metric.sqale_index.name + : + – + </div> + <div> + metric.ncloc.name + : + 1.7short_number_suffix.k + </div> + </div>, "x": 0, "y": 53.5, }, diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap index b6e9c87e444..1c661eb1690 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap @@ -13,7 +13,7 @@ exports[`renders 1`] = ` items={ Array [ Object { - "color": undefined, + "color": "#b0d513", "key": "foo", "link": Object { "pathname": "/dashboard", @@ -22,7 +22,37 @@ exports[`renders 1`] = ` }, }, "size": 1734, - "tooltip": "<div class=\\"text-left\\"><div class=\\"little-spacer-bottom\\"><strong>Foo</strong></div><div>metric.complexity.name: 17</div><div>metric.coverage.name: 53.5%</div><div>metric.ncloc.name: 1,734</div></div>", + "tooltip": <div + className="text-left" + > + <div + className="little-spacer-bottom" + > + <strong> + Foo + </strong> + </div> + <div> + metric.complexity.name + : + 17 + </div> + <div> + metric.coverage.name + : + 53.5% + </div> + <div> + metric.ncloc.name + : + 1,734 + </div> + <div> + metric.security_rating.name + : + B + </div> + </div>, "x": 17.2, "y": 53.5, }, diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index 2c58557ac4f..4a66fe59a8c 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -76,7 +76,7 @@ export default class ProjectRowActions extends React.PureComponent<Props, State> ); }; - handleDropdownClick = () => { + handleDropdownOpen = () => { if (this.state.hasAccess === undefined && !this.state.loading) { this.fetchPermissions(); } @@ -106,7 +106,7 @@ export default class ProjectRowActions extends React.PureComponent<Props, State> const { hasAccess } = this.state; return ( - <ActionsDropdown onToggleClick={this.handleDropdownClick}> + <ActionsDropdown onOpen={this.handleDropdownOpen}> {hasAccess === true && ( <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}> {translate('edit_permissions')} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx index 4c9e74dc1b1..b30e553d91d 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx @@ -44,7 +44,7 @@ it('restores access', async () => { const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); - wrapper.prop<Function>('onToggleClick')(); + wrapper.prop<Function>('onOpen')(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap index b824855ce96..61ea2fee3de 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap @@ -19,7 +19,7 @@ exports[`applies permission template 1`] = ` exports[`restores access 1`] = ` <ActionsDropdown - onToggleClick={[Function]} + onOpen={[Function]} > <ActionsDropdownItem className="js-apply-template" @@ -32,7 +32,7 @@ exports[`restores access 1`] = ` exports[`restores access 2`] = ` <ActionsDropdown - onToggleClick={[Function]} + onOpen={[Function]} > <ActionsDropdownItem className="js-restore-access" @@ -51,7 +51,7 @@ exports[`restores access 2`] = ` exports[`restores access 3`] = ` <ActionsDropdown - onToggleClick={[Function]} + onOpen={[Function]} > <ActionsDropdownItem className="js-restore-access" diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx index 994946161c3..80ef81fbb54 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx @@ -19,8 +19,10 @@ */ import * as React from 'react'; import { IndexLink } from 'react-router'; +import * as classNames from 'classnames'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getProfilesPath, getProfilesForLanguagePath } from '../utils'; +import Dropdown from '../../../components/controls/Dropdown'; interface Props { currentFilter?: string; @@ -28,67 +30,55 @@ interface Props { organization: string | null; } -export default class ProfilesListHeader extends React.PureComponent<Props> { - renderFilterToggle() { - const { languages, currentFilter } = this.props; - const currentLanguage = currentFilter && languages.find(l => l.key === currentFilter); - - const label = currentLanguage - ? translateWithParameters('quality_profiles.x_Profiles', currentLanguage.name) - : translate('quality_profiles.all_profiles'); - - return ( - <a - className="dropdown-toggle link-no-underline js-language-filter" - href="#" - data-toggle="dropdown"> - {label} <i className="icon-dropdown" /> - </a> - ); +export default function ProfilesListHeader({ currentFilter, languages, organization }: Props) { + if (languages.length < 2) { + return null; } - renderFilterMenu() { - return ( - <ul className="dropdown-menu"> - <li> - <IndexLink to={getProfilesPath(this.props.organization)}> - {translate('quality_profiles.all_profiles')} - </IndexLink> - </li> - {this.props.languages.map(language => ( - <li key={language.key}> - <IndexLink - to={getProfilesForLanguagePath(language.key, this.props.organization)} - className="js-language-filter-option" - data-language={language.key}> - {language.name} - </IndexLink> - </li> - ))} - </ul> - ); - } + const currentLanguage = currentFilter && languages.find(l => l.key === currentFilter); - render() { - if (this.props.languages.length < 2) { - return null; - } + // if unknown language, then + if (currentFilter && !currentLanguage) { + return null; + } - const { languages, currentFilter } = this.props; - const currentLanguage = currentFilter && languages.find(l => l.key === currentFilter); + const label = currentLanguage + ? translateWithParameters('quality_profiles.x_Profiles', currentLanguage.name) + : translate('quality_profiles.all_profiles'); - // if unknown language, then - if (currentFilter && !currentLanguage) { - return null; - } + return ( + <header className="quality-profiles-list-header clearfix"> + <Dropdown> + {({ onToggleClick, open }) => ( + <div className={classNames('dropdown', { open })}> + <a + className="dropdown-toggle link-no-underline js-language-filter" + href="#" + onClick={onToggleClick}> + {label} + <i className="icon-dropdown little-spacer-left" /> + </a> - return ( - <header className="quality-profiles-list-header clearfix"> - <div className="dropdown"> - {this.renderFilterToggle()} - {this.renderFilterMenu()} - </div> - </header> - ); - } + <ul className="dropdown-menu"> + <li> + <IndexLink to={getProfilesPath(organization)}> + {translate('quality_profiles.all_profiles')} + </IndexLink> + </li> + {languages.map(language => ( + <li key={language.key}> + <IndexLink + className="js-language-filter-option" + data-language={language.key} + to={getProfilesForLanguagePath(language.key, organization)}> + {language.name} + </IndexLink> + </li> + ))} + </ul> + </div> + )} + </Dropdown> + </header> + ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index 7b718fc5ad2..e34bbf7e10e 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -18,9 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import ChangeLogLevelForm from './ChangeLogLevelForm'; import RestartForm from '../../../components/common/RestartForm'; import { getFileNameSuffix } from '../utils'; +import Dropdown from '../../../components/controls/Dropdown'; import { EditButton, Button } from '../../../components/ui/buttons'; import { getBaseUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; @@ -100,51 +102,54 @@ export default class PageActions extends React.PureComponent<Props, State> { /> </span> {this.props.canDownloadLogs && ( - <div className="display-inline-block dropdown spacer-left"> - {/* TODO use Dropdown component */} - <Button data-toggle="dropdown"> - {translate('system.download_logs')} - <i className="icon-dropdown little-spacer-left" /> - </Button> - <ul className="dropdown-menu"> - <li> - <a - download="sonarqube_app.log" - href={logsUrl + '?process=app'} - id="logs-link" - target="_blank"> - Main Process - </a> - </li> - <li> - <a - download="sonarqube_ce.log" - href={logsUrl + '?process=ce'} - id="ce-logs-link" - target="_blank"> - Compute Engine - </a> - </li> - <li> - <a - download="sonarqube_es.log" - href={logsUrl + '?process=es'} - id="es-logs-link" - target="_blank"> - Search Engine - </a> - </li> - <li> - <a - download="sonarqube_web.log" - href={logsUrl + '?process=web'} - id="web-logs-link" - target="_blank"> - Web Server - </a> - </li> - </ul> - </div> + <Dropdown> + {({ onToggleClick, open }) => ( + <div className={classNames('display-inline-block dropdown spacer-left', { open })}> + <Button onClick={onToggleClick}> + {translate('system.download_logs')} + <i className="icon-dropdown little-spacer-left" /> + </Button> + <ul className="dropdown-menu"> + <li> + <a + download="sonarqube_app.log" + href={logsUrl + '?process=app'} + id="logs-link" + target="_blank"> + Main Process + </a> + </li> + <li> + <a + download="sonarqube_ce.log" + href={logsUrl + '?process=ce'} + id="ce-logs-link" + target="_blank"> + Compute Engine + </a> + </li> + <li> + <a + download="sonarqube_es.log" + href={logsUrl + '?process=es'} + id="es-logs-link" + target="_blank"> + Search Engine + </a> + </li> + <li> + <a + download="sonarqube_web.log" + href={logsUrl + '?process=web'} + id="web-logs-link" + target="_blank"> + Web Server + </a> + </li> + </ul> + </div> + )} + </Dropdown> )} <a className="button spacer-left" diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx index 5bf79f1de3d..44f9d93a7a8 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx @@ -27,7 +27,9 @@ jest.mock('../../utils', () => ({ })); it('should render correctly', () => { - expect(getWrapper({ serverId: 'MyServerId' })).toMatchSnapshot(); + const wrapper = getWrapper({ serverId: 'MyServerId' }); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); }); it('should render without restart and log download', () => { diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap index 252b7a95398..3c698c9ba19 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageActions-test.tsx.snap @@ -22,62 +22,7 @@ exports[`should render correctly 1`] = ` onClick={[Function]} /> </span> - <div - className="display-inline-block dropdown spacer-left" - > - <Button - data-toggle="dropdown" - > - system.download_logs - <i - className="icon-dropdown little-spacer-left" - /> - </Button> - <ul - className="dropdown-menu" - > - <li> - <a - download="sonarqube_app.log" - href="/api/system/logs?process=app" - id="logs-link" - target="_blank" - > - Main Process - </a> - </li> - <li> - <a - download="sonarqube_ce.log" - href="/api/system/logs?process=ce" - id="ce-logs-link" - target="_blank" - > - Compute Engine - </a> - </li> - <li> - <a - download="sonarqube_es.log" - href="/api/system/logs?process=es" - id="es-logs-link" - target="_blank" - > - Search Engine - </a> - </li> - <li> - <a - download="sonarqube_web.log" - href="/api/system/logs?process=web" - id="web-logs-link" - target="_blank" - > - Web Server - </a> - </li> - </ul> - </div> + <Dropdown /> <a className="button spacer-left" download="sonarqube-support-info-filesuffix(MyServerId).json" @@ -98,6 +43,65 @@ exports[`should render correctly 1`] = ` </div> `; +exports[`should render correctly 2`] = ` +<div + className="display-inline-block dropdown spacer-left" +> + <Button + onClick={[Function]} + > + system.download_logs + <i + className="icon-dropdown little-spacer-left" + /> + </Button> + <ul + className="dropdown-menu" + > + <li> + <a + download="sonarqube_app.log" + href="/api/system/logs?process=app" + id="logs-link" + target="_blank" + > + Main Process + </a> + </li> + <li> + <a + download="sonarqube_ce.log" + href="/api/system/logs?process=ce" + id="ce-logs-link" + target="_blank" + > + Compute Engine + </a> + </li> + <li> + <a + download="sonarqube_es.log" + href="/api/system/logs?process=es" + id="es-logs-link" + target="_blank" + > + Search Engine + </a> + </li> + <li> + <a + download="sonarqube_web.log" + href="/api/system/logs?process=web" + id="web-logs-link" + target="_blank" + > + Web Server + </a> + </li> + </ul> +</div> +`; + exports[`should render without restart and log download 1`] = ` <div className="page-actions" diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index 72ac6fa5bcd..f0fe9ec18ab 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -21,10 +21,14 @@ import { stringify } from 'querystring'; import * as React from 'react'; import { Link } from 'react-router'; import * as PropTypes from 'prop-types'; +import * as classNames from 'classnames'; import MeasuresOverlay from './components/MeasuresOverlay'; import { SourceViewerFile, BranchLike } from '../../app/types'; import QualifierIcon from '../shared/QualifierIcon'; +import Dropdown from '../controls/Dropdown'; import FavoriteContainer from '../controls/FavoriteContainer'; +import ListIcon from '../icons-components/ListIcon'; +import { ButtonIcon } from '../ui/buttons'; import { WorkspaceContext } from '../workspace/context'; import { getPathUrlAsString, @@ -124,50 +128,55 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State </div> </div> - <div className="dropdown source-viewer-header-actions"> - <a - className="js-actions icon-list dropdown-toggle" - data-toggle="dropdown" - title={translate('component_viewer.more_actions')} - /> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}> - {translate('component_viewer.show_details')} - </a> - {this.state.measuresOverlay && ( - <MeasuresOverlay - branchLike={this.props.branchLike} - onClose={this.handleMeasuresOverlayClose} - sourceViewerFile={this.props.sourceViewerFile} - /> - )} - </li> - <li> - <a - className="js-new-window" - href={getPathUrlAsString({ - pathname: '/component', - query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } - })} - target="_blank"> - {translate('component_viewer.new_window')} - </a> - </li> - {!workspace && ( - <li> - <a className="js-workspace" href="#" onClick={this.openInWorkspace}> - {translate('component_viewer.open_in_workspace')} - </a> - </li> - )} - <li> - <a className="js-raw-source" href={rawSourcesLink} target="_blank"> - {translate('component_viewer.show_raw_source')} - </a> - </li> - </ul> - </div> + <Dropdown> + {({ onToggleClick, open }) => ( + <div className={classNames('dropdown source-viewer-header-actions', { open })}> + <ButtonIcon + className="js-actions" + onClick={onToggleClick} + tooltip={translate('component_viewer.more_actions')}> + <ListIcon /> + </ButtonIcon> + <ul className="dropdown-menu dropdown-menu-right"> + <li> + <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}> + {translate('component_viewer.show_details')} + </a> + {this.state.measuresOverlay && ( + <MeasuresOverlay + branchLike={this.props.branchLike} + onClose={this.handleMeasuresOverlayClose} + sourceViewerFile={this.props.sourceViewerFile} + /> + )} + </li> + <li> + <a + className="js-new-window" + href={getPathUrlAsString({ + pathname: '/component', + query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } + })} + target="_blank"> + {translate('component_viewer.new_window')} + </a> + </li> + {!workspace && ( + <li> + <a className="js-workspace" href="#" onClick={this.openInWorkspace}> + {translate('component_viewer.open_in_workspace')} + </a> + </li> + )} + <li> + <a className="js-raw-source" href={rawSourcesLink} target="_blank"> + {translate('component_viewer.show_raw_source')} + </a> + </li> + </ul> + </div> + )} + </Dropdown> <div className="source-viewer-header-measures"> {isUnitTest && ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css index 73521086ad3..60a57ab69ba 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -336,7 +336,11 @@ float: right; display: block; margin-left: 25px; - padding: 13px 5px; + padding: 8px 5px; +} + +.source-viewer-header-actions svg { + margin-top: 2px; } .source-viewer-header-more-actions { diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts b/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts index d2f4947fd5f..f428115c4b3 100644 --- a/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts +++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts @@ -26,7 +26,7 @@ interface Item { color?: string; key?: string; link?: any; - tooltip?: string; + tooltip?: React.ReactNode; } interface Props { diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.js b/server/sonar-web/src/main/js/components/charts/BubbleChart.js index 6245210b1b0..9cb5681debf 100644 --- a/server/sonar-web/src/main/js/components/charts/BubbleChart.js +++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.js @@ -24,7 +24,7 @@ import { min, max } from 'd3-array'; import { scaleLinear } from 'd3-scale'; import { sortBy, uniq } from 'lodash'; import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; -import { TooltipsContainer } from '../mixins/tooltips-mixin'; +import Tooltip from '../controls/Tooltip'; /*:: type Scale = { @@ -42,7 +42,7 @@ export class Bubble extends React.PureComponent { link?: string, onClick: (?string) => void, r: number, - tooltip?: string, + tooltip?: string | React$Element<*>, x: number, y: number }; @@ -55,23 +55,12 @@ export class Bubble extends React.PureComponent { }; render() { - const tooltipAttrs = this.props.tooltip - ? { - 'data-toggle': 'tooltip', - title: this.props.tooltip - } - : {}; - let circle = ( <circle - {...tooltipAttrs} - onClick={this.props.onClick ? this.handleClick : undefined} className="bubble-chart-bubble" + onClick={this.props.onClick ? this.handleClick : undefined} r={this.props.r} - style={{ - fill: this.props.color, - stroke: this.props.color - }} + style={{ fill: this.props.color, stroke: this.props.color }} transform={`translate(${this.props.x}, ${this.props.y})`} /> ); @@ -81,9 +70,9 @@ export class Bubble extends React.PureComponent { } return this.props.tooltip ? ( - <TooltipsContainer> + <Tooltip overlay={this.props.tooltip}> <g>{circle}</g> - </TooltipsContainer> + </Tooltip> ) : ( circle ); @@ -99,7 +88,7 @@ export default class BubbleChart extends React.PureComponent { color?: string, key?: string, link?: string, - tooltip?: string + tooltip?: string | React$Element<*> |}>, sizeRange?: [number, number], displayXGrid: boolean, diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/work-cloud-test.js b/server/sonar-web/src/main/js/components/charts/__tests__/work-cloud-test.js deleted file mode 100644 index 61c92f9baf3..00000000000 --- a/server/sonar-web/src/main/js/components/charts/__tests__/work-cloud-test.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { WordCloud, Word } from '../word-cloud'; - -it('should display', () => { - const items = [ - { size: 10, link: '#', text: 'SonarQube :: Server' }, - { size: 30, link: '#', text: 'SonarQube :: Web' }, - { size: 20, link: '#', text: 'SonarQube :: Search' } - ]; - const chart = shallow(<WordCloud items={items} width={100} height={100} />); - expect(chart.find(Word).length).toBe(3); -}); diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js index 14bec0cad3e..a312cefc95a 100644 --- a/server/sonar-web/src/main/js/components/charts/bar-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -22,8 +22,8 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { max } from 'd3-array'; import { scaleLinear, scaleBand } from 'd3-scale'; -import { ResizeMixin } from './../mixins/resize-mixin'; -import { TooltipsContainer } from './../mixins/tooltips-mixin'; +import Tooltip from '../controls/Tooltip'; +import { ResizeMixin } from '../mixins/resize-mixin'; export const BarChart = createReactClass({ displayName: 'BarChart', @@ -74,24 +74,25 @@ export const BarChart = createReactClass({ const x = Math.round(xScale(point.x) + xScale.bandwidth() / 2); const y = yScale.range()[0]; const d = this.props.data[index]; - const tooltipAtts = {}; - if (d.tooltip) { - tooltipAtts['title'] = d.tooltip; - tooltipAtts['data-toggle'] = 'tooltip'; - } - return ( + const text = ( <text - {...tooltipAtts} - key={index} className="bar-chart-tick" - x={x} - y={y} dy="1.5em" + key={index} onClick={this.props.onBarClick && this.handleClick.bind(this, point)} - style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}> + style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }} + x={x} + y={y}> {tick} </text> ); + return d.tooltip ? ( + <Tooltip key={index} overlay={d.tooltip}> + {text} + </Tooltip> + ) : ( + text + ); }); return <g>{ticks}</g>; }, @@ -105,24 +106,25 @@ export const BarChart = createReactClass({ const x = Math.round(xScale(point.x) + xScale.bandwidth() / 2); const y = yScale(point.y); const d = this.props.data[index]; - const tooltipAtts = {}; - if (d.tooltip) { - tooltipAtts['title'] = d.tooltip; - tooltipAtts['data-toggle'] = 'tooltip'; - } - return ( + const text = ( <text - key={index} className="bar-chart-tick" - x={x} - y={y} dy="-1em" + key={index} onClick={this.props.onBarClick && this.handleClick.bind(this, point)} style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }} - {...tooltipAtts}> + x={x} + y={y}> {value} </text> ); + return d.tooltip ? ( + <Tooltip key={index} overlay={d.tooltip}> + {text} + </Tooltip> + ) : ( + text + ); }); return <g>{ticks}</g>; }, @@ -133,24 +135,25 @@ export const BarChart = createReactClass({ const maxY = yScale.range()[0]; const y = Math.round(yScale(d.y)) - /* minimum bar height */ 1; const height = maxY - y; - const tooltipAtts = {}; - if (d.tooltip) { - tooltipAtts['title'] = d.tooltip; - tooltipAtts['data-toggle'] = 'tooltip'; - } - return ( + const rect = ( <rect - key={index} className="bar-chart-bar" - {...tooltipAtts} - x={x} - y={y} - width={this.props.barsWidth} height={height} + key={index} onClick={this.props.onBarClick && this.handleClick.bind(this, d)} style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }} + width={this.props.barsWidth} + x={x} + y={y} /> ); + return d.tooltip ? ( + <Tooltip key={index} overlay={d.tooltip}> + {rect} + </Tooltip> + ) : ( + rect + ); }); return <g>{bars}</g>; }, @@ -178,15 +181,13 @@ export const BarChart = createReactClass({ .range([availableHeight, 0]); return ( - <TooltipsContainer> - <svg className="bar-chart" width={this.state.width} height={this.state.height}> - <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> - {this.renderXTicks(xScale, yScale)} - {this.renderXValues(xScale, yScale)} - {this.renderBars(xScale, yScale)} - </g> - </svg> - </TooltipsContainer> + <svg className="bar-chart" height={this.state.height} width={this.state.width}> + <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> + {this.renderXTicks(xScale, yScale)} + {this.renderXValues(xScale, yScale)} + {this.renderBars(xScale, yScale)} + </g> + </svg> ); } }); diff --git a/server/sonar-web/src/main/js/components/charts/donut-chart.js b/server/sonar-web/src/main/js/components/charts/donut-chart.js index eaa0c4ce6fd..08fd9b0bb24 100644 --- a/server/sonar-web/src/main/js/components/charts/donut-chart.js +++ b/server/sonar-web/src/main/js/components/charts/donut-chart.js @@ -22,7 +22,6 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { arc as d3Arc, pie as d3Pie } from 'd3-shape'; import { ResizeMixin } from './../mixins/resize-mixin'; -import { TooltipsMixin } from './../mixins/tooltips-mixin'; function Sector(props) { const arc = d3Arc() @@ -38,7 +37,7 @@ export const DonutChart = createReactClass({ data: PropTypes.arrayOf(PropTypes.object).isRequired }, - mixins: [ResizeMixin, TooltipsMixin], + mixins: [ResizeMixin], getDefaultProps() { return { thickness: 6, padding: [0, 0, 0, 0] }; @@ -65,17 +64,17 @@ export const DonutChart = createReactClass({ const sectors = pie(this.props.data).map((d, i) => { return ( <Sector - key={i} data={d} - radius={radius} fill={this.props.data[i].fill} + key={i} + radius={radius} thickness={this.props.thickness} /> ); }); return ( - <svg className="donut-chart" width={this.state.width} height={this.state.height}> + <svg className="donut-chart" height={this.state.height} width={this.state.width}> <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> <g transform={`translate(${radius}, ${radius})`}>{sectors}</g> </g> diff --git a/server/sonar-web/src/main/js/components/charts/line-chart.js b/server/sonar-web/src/main/js/components/charts/line-chart.js index bb9a7a88098..af99622368b 100644 --- a/server/sonar-web/src/main/js/components/charts/line-chart.js +++ b/server/sonar-web/src/main/js/components/charts/line-chart.js @@ -24,7 +24,6 @@ import { extent, max } from 'd3-array'; import { scaleLinear } from 'd3-scale'; import { area as d3Area, line as d3Line, curveBasis } from 'd3-shape'; import { ResizeMixin } from './../mixins/resize-mixin'; -import { TooltipsMixin } from './../mixins/tooltips-mixin'; export const LineChart = createReactClass({ displayName: 'LineChart', @@ -41,7 +40,7 @@ export const LineChart = createReactClass({ height: PropTypes.number }, - mixins: [ResizeMixin, TooltipsMixin], + mixins: [ResizeMixin], getDefaultProps() { return { diff --git a/server/sonar-web/src/main/js/components/charts/word-cloud.js b/server/sonar-web/src/main/js/components/charts/word-cloud.js deleted file mode 100644 index 71f9ca10faf..00000000000 --- a/server/sonar-web/src/main/js/components/charts/word-cloud.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { max } from 'd3-array'; -import { scaleLinear } from 'd3-scale'; -import { sortBy } from 'lodash'; -import { TooltipsMixin } from './../mixins/tooltips-mixin'; - -export function Word(props) { - let tooltipAttrs = {}; - if (props.tooltip) { - tooltipAttrs = { - 'data-toggle': 'tooltip', - title: props.tooltip - }; - } - return ( - <a {...tooltipAttrs} style={{ fontSize: props.size }} href={props.link}> - {props.text} - </a> - ); -} - -Word.propTypes = { - size: PropTypes.number.isRequired, - text: PropTypes.string.isRequired, - tooltip: PropTypes.string, - link: PropTypes.string.isRequired -}; - -export const WordCloud = createReactClass({ - displayName: 'WordCloud', - - propTypes: { - items: PropTypes.arrayOf(PropTypes.object).isRequired, - sizeRange: PropTypes.arrayOf(PropTypes.number) - }, - - mixins: [TooltipsMixin], - - getDefaultProps() { - return { - sizeRange: [10, 24] - }; - }, - - render() { - const len = this.props.items.length; - const sortedItems = sortBy(this.props.items, (item, idx) => { - const index = len - idx; - return (index % 2) * (len - index) + index / 2; - }); - - const sizeScale = scaleLinear() - .domain([0, max(this.props.items, d => d.size)]) - .range(this.props.sizeRange); - const words = sortedItems.map((item, index) => ( - <Word - key={index} - text={item.text} - size={sizeScale(item.size)} - link={item.link} - tooltip={item.tooltip} - /> - )); - return <div className="word-cloud">{words}</div>; - } -}); diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx index a20842524a6..f1173c88e75 100644 --- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { Link } from 'react-router'; import { LocationDescriptor } from 'history'; +import Dropdown from './Dropdown'; import SettingsIcon from '../icons-components/SettingsIcon'; import { Button } from '../ui/buttons'; @@ -29,31 +30,33 @@ interface Props { children: React.ReactNode; menuClassName?: string; menuPosition?: 'left' | 'right'; - // TODO: replace with `onOpen` & `onClose` - onToggleClick?: () => void; + onOpen?: () => void; small?: boolean; toggleClassName?: string; } export default function ActionsDropdown({ menuPosition = 'right', ...props }: Props) { return ( - <div className={classNames('dropdown', props.className)}> - <Button - className={classNames('dropdown-toggle', props.toggleClassName, { - 'button-small': props.small - })} - data-toggle="dropdown" - onClick={props.onToggleClick}> - <SettingsIcon className="text-text-bottom" /> - <i className="icon-dropdown little-spacer-left" /> - </Button> - <ul - className={classNames('dropdown-menu', props.menuClassName, { - 'dropdown-menu-right': menuPosition === 'right' - })}> - {props.children} - </ul> - </div> + <Dropdown onOpen={props.onOpen}> + {({ onToggleClick, open }) => ( + <div className={classNames('dropdown', props.className, { open })}> + <Button + className={classNames('dropdown-toggle', props.toggleClassName, { + 'button-small': props.small + })} + onClick={onToggleClick}> + <SettingsIcon className="text-text-bottom" /> + <i className="icon-dropdown little-spacer-left" /> + </Button> + <ul + className={classNames('dropdown-menu', props.menuClassName, { + 'dropdown-menu-right': menuPosition === 'right' + })}> + {props.children} + </ul> + </div> + )} + </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js deleted file mode 100644 index ae61a8f868f..00000000000 --- a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -import $ from 'jquery'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -export const TooltipsMixin = { - componentDidMount() { - this.initTooltips(); - }, - - componentWillUpdate() { - this.hideTooltips(); - }, - - componentDidUpdate() { - this.initTooltips(); - }, - - componentWillUnmount() { - this.destroyTooltips(); - }, - - initTooltips() { - if ($.fn && $.fn.tooltip) { - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip({ - container: 'body', - placement: 'bottom', - html: true - }); - } - }, - - hideTooltips() { - if ($.fn && $.fn.tooltip) { - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip('hide'); - } - }, - - destroyTooltips() { - if ($.fn && $.fn.tooltip) { - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip('destroy'); - } - } -}; - -export class TooltipsContainer extends React.PureComponent { - componentDidMount() { - this.initTooltips(); - } - - componentWillUpdate() { - this.destroyTooltips(); - } - - componentDidUpdate() { - this.initTooltips(); - } - - componentWillUnmount() { - this.destroyTooltips(); - } - - initTooltips = () => { - if ($.fn && $.fn.tooltip) { - const options = Object.assign( - { container: 'body', placement: 'bottom', html: true }, - this.props.options - ); - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip(options); - } - }; - - hideTooltips = () => { - if ($.fn && $.fn.tooltip) { - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip('hide'); - } - }; - - destroyTooltips = () => { - if ($.fn && $.fn.tooltip) { - $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip('destroy'); - } - }; - - render() { - return this.props.children; - } -} diff --git a/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js b/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js deleted file mode 100644 index 31ad96f632b..00000000000 --- a/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { BarChart } from '../charts/bar-chart'; -import { formatMeasure } from '../../helpers/measures'; -import { translateWithParameters } from '../../helpers/l10n'; - -const HEIGHT = 80; - -export default class ComplexityDistribution extends React.PureComponent { - static propTypes = { - distribution: PropTypes.string.isRequired, - of: PropTypes.string.isRequired - }; - - renderBarChart = () => { - const data = this.props.distribution.split(';').map((point, index) => { - const tokens = point.split('='); - const y = parseInt(tokens[1], 10); - const value = parseInt(tokens[0], 10); - return { - x: index, - y, - value, - tooltip: translateWithParameters(`overview.complexity_tooltip.${this.props.of}`, y, value) - }; - }); - - const xTicks = data.map(point => point.value); - - const xValues = data.map(point => formatMeasure(point.y, 'INT')); - - return ( - <BarChart - data={data} - xTicks={xTicks} - xValues={xValues} - height={HEIGHT} - barsWidth={20} - padding={[25, 10, 25, 10]} - /> - ); - }; - - render() { - return ( - <div className="overview-bar-chart" style={{ height: HEIGHT }}> - {this.renderBarChart()} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/libs/third-party/bootstrap/dropdown.js b/server/sonar-web/src/main/js/libs/third-party/bootstrap/dropdown.js deleted file mode 100644 index 3573bd500ec..00000000000 --- a/server/sonar-web/src/main/js/libs/third-party/bootstrap/dropdown.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -+function ($) { - 'use strict'; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.VERSION = '3.3.1' - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we use a backdrop because click events don't delegate - $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) - } - - var relatedTarget = { relatedTarget: this } - $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this - .trigger('focus') - .attr('aria-expanded', 'true') - - $parent - .toggleClass('open') - .trigger('shown.bs.dropdown', relatedTarget) - } - - return false - } - - Dropdown.prototype.keydown = function (e) { - if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return - - var $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - if ((!isActive && e.which != 27) || (isActive && e.which == 27)) { - if (e.which == 27) $parent.find(toggle).trigger('focus') - return $this.trigger('click') - } - - var desc = ' li:not(.divider):visible a' - var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc) - - if (!$items.length) return - - var index = $items.index(e.target) - - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items.eq(index).trigger('focus') - } - - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) - }) - } - - function getParent($this) { - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = selector && $(selector) - - return $parent && $parent.length ? $parent : $this.parent() - } - - - // DROPDOWN PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.dropdown') - - if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.dropdown - - $.fn.dropdown = Plugin - $.fn.dropdown.Constructor = Dropdown - - - // DROPDOWN NO CONFLICT - // ==================== - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - // APPLY TO STANDARD DROPDOWN ELEMENTS - // =================================== - - $(document) - .on('click.bs.dropdown.data-api', clearMenus) - .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) - .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown) - -}(jQuery); diff --git a/server/sonar-web/src/main/js/libs/third-party/bootstrap/tooltip.js b/server/sonar-web/src/main/js/libs/third-party/bootstrap/tooltip.js deleted file mode 100644 index 535ce425728..00000000000 --- a/server/sonar-web/src/main/js/libs/third-party/bootstrap/tooltip.js +++ /dev/null @@ -1,487 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = - this.options = - this.enabled = - this.timeout = - this.hoverState = - this.$element = null - - this.init('tooltip', element, options) - } - - Tooltip.VERSION = '3.3.1' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport) - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (self && self.$tip && self.$tip.is(':visible')) { - self.hoverState = 'in' - return - } - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this - - var $tip = this.tip() - - var tipId = this.getUID(this.type) - - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - .data('bs.' + this.type, this) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var orgPlacement = placement - var $container = this.options.container ? $(this.options.container) : this.$element.parent() - var containerDim = this.getPosition($container) - - placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - - if (prevHoverState == 'out') that.leave(that) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top = offset.top + marginTop - offset.left = offset.left + marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } - - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - - if (delta.left) offset.left += delta.left - else offset.top += delta.top - - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) { - this.arrow() - .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isHorizontal ? 'top' : 'left', '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = this.tip() - var e = $.Event('hide.bs.' + this.type) - - function complete() { - $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - - var el = $element[0] - var isBody = el.tagName == 'BODY' - - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) - } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - - } - - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } - } - - return delta - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix - } - - Tooltip.prototype.tip = function () { - return (this.$tip = this.$tip || $(this.options.template)) - } - - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } - - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - }) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - var selector = options && options.selector - - if (!data && option == 'destroy') return - if (selector) { - if (!data) $this.data('bs.tooltip', (data = {})) - if (!data[selector]) data[selector] = new Tooltip(this, options) - } else { - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - } - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tooltip - - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); diff --git a/server/sonar-web/src/main/js/libs/third-party/select2.js b/server/sonar-web/src/main/js/libs/third-party/select2.js deleted file mode 100644 index 26488c4b888..00000000000 --- a/server/sonar-web/src/main/js/libs/third-party/select2.js +++ /dev/null @@ -1,2429 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser 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. - */ -/* - Copyright 2012 Igor Vaynberg - - Version: 3.2 Timestamp: Mon Sep 10 10:38:04 PDT 2012 - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in - compliance with the License. You may obtain a copy of the License in the LICENSE file, or 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. - */ - (function ($) { - if(typeof $.fn.each2 == "undefined"){ - $.fn.extend({ - /* - * 4-10 times faster .each replacement - * use it carefully, as it overrides jQuery context of element on each iteration - */ - each2 : function (c) { - var j = $([0]), i = -1, l = this.length; - while ( - ++i < l - && (j.context = j[0] = this[i]) - && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object - ); - return this; - } - }); - } -})(jQuery); - -(function ($, undefined) { - "use strict"; - /*global document, window, jQuery, console */ - - if (window.Select2 !== undefined) { - return; - } - - var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer; - - KEY = { - TAB: 9, - ENTER: 13, - ESC: 27, - SPACE: 32, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - SHIFT: 16, - CTRL: 17, - ALT: 18, - PAGE_UP: 33, - PAGE_DOWN: 34, - HOME: 36, - END: 35, - BACKSPACE: 8, - DELETE: 46, - isArrow: function (k) { - k = k.which ? k.which : k; - switch (k) { - case KEY.LEFT: - case KEY.RIGHT: - case KEY.UP: - case KEY.DOWN: - return true; - } - return false; - }, - isControl: function (e) { - var k = e.which; - switch (k) { - case KEY.SHIFT: - case KEY.CTRL: - case KEY.ALT: - return true; - } - - if (e.metaKey) return true; - - return false; - }, - isFunctionKey: function (k) { - k = k.which ? k.which : k; - return k >= 112 && k <= 123; - } - }; - - nextUid=(function() { var counter=1; return function() { return counter++; }; }()); - - function indexOf(value, array) { - var i = 0, l = array.length, v; - - if (typeof value === "undefined") { - return -1; - } - - if (value.constructor === String) { - for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i; - } else { - for (; i < l; i = i + 1) { - v = array[i]; - if (v.constructor === String) { - if (v.localeCompare(value) === 0) return i; - } else { - if (v === value) return i; - } - } - } - return -1; - } - - /** - * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used - * @param a - * @param b - */ - function equal(a, b) { - if (a === b) return true; - if (a === undefined || b === undefined) return false; - if (a === null || b === null) return false; - if (a.constructor === String) return a.localeCompare(b) === 0; - if (b.constructor === String) return b.localeCompare(a) === 0; - return false; - } - - /** - * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty - * strings - * @param string - * @param separator - */ - function splitVal(string, separator) { - var val, i, l; - if (string === null || string.length < 1) return []; - val = string.split(separator); - for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); - return val; - } - - function getSideBorderPadding(element) { - return element.outerWidth() - element.width(); - } - - function installKeyUpChangeEvent(element) { - var key="keyup-change-value"; - element.bind("keydown", function () { - if ($.data(element, key) === undefined) { - $.data(element, key, element.val()); - } - }); - element.bind("keyup", function () { - var val= $.data(element, key); - if (val !== undefined && element.val() !== val) { - $.removeData(element, key); - element.trigger("keyup-change"); - } - }); - } - - $(document).delegate("body", "mousemove", function (e) { - $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY}); - }); - - /** - * filters mouse events so an event is fired only if the mouse moved. - * - * filters out mouse events that occur when mouse is stationary but - * the elements under the pointer are scrolled. - */ - function installFilteredMouseMove(element) { - element.bind("mousemove", function (e) { - var lastpos = $.data(document, "select2-lastpos"); - if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { - $(e.target).trigger("mousemove-filtered", e); - } - }); - } - - /** - * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made - * within the last quietMillis milliseconds. - * - * @param quietMillis number of milliseconds to wait before invoking fn - * @param fn function to be debounced - * @param ctx object to be used as this reference within fn - * @return debounced version of fn - */ - function debounce(quietMillis, fn, ctx) { - ctx = ctx || undefined; - var timeout; - return function () { - var args = arguments; - window.clearTimeout(timeout); - timeout = window.setTimeout(function() { - fn.apply(ctx, args); - }, quietMillis); - }; - } - - /** - * A simple implementation of a thunk - * @param formula function used to lazily initialize the thunk - * @return {Function} - */ - function thunk(formula) { - var evaluated = false, - value; - return function() { - if (evaluated === false) { value = formula(); evaluated = true; } - return value; - }; - }; - - function installDebouncedScroll(threshold, element) { - var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); - element.bind("scroll", function (e) { - if (indexOf(e.target, element.get()) >= 0) notify(e); - }); - } - - function killEvent(event) { - event.preventDefault(); - event.stopPropagation(); - } - - function measureTextWidth(e) { - if (!sizer){ - var style = e[0].currentStyle || window.getComputedStyle(e[0], null); - sizer = $("<div></div>").css({ - position: "absolute", - left: "-10000px", - top: "-10000px", - display: "none", - fontSize: style.fontSize, - fontFamily: style.fontFamily, - fontStyle: style.fontStyle, - fontWeight: style.fontWeight, - letterSpacing: style.letterSpacing, - textTransform: style.textTransform, - whiteSpace: "nowrap" - }); - $("body").append(sizer); - } - sizer.text(e.val()); - return sizer.width(); - } - - function markMatch(text, term, markup) { - var match=text.toUpperCase().indexOf(term.toUpperCase()), - tl=term.length; - - if (match<0) { - markup.push(text); - return; - } - - markup.push(text.substring(0, match)); - markup.push("<span class='select2-match'>"); - markup.push(text.substring(match, match + tl)); - markup.push("</span>"); - markup.push(text.substring(match + tl, text.length)); - } - - /** - * Produces an ajax-based query function - * - * @param options object containing configuration paramters - * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax - * @param options.url url for the data - * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. - * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified - * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request - * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often - * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. - * The expected format is an object containing the following keys: - * results array of objects that will be used as choices - * more (optional) boolean indicating whether there are more results available - * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} - */ - function ajax(options) { - var timeout, // current scheduled but not yet executed request - requestSequence = 0, // sequence used to drop out-of-order responses - handler = null, - quietMillis = options.quietMillis || 100; - - return function (query) { - window.clearTimeout(timeout); - timeout = window.setTimeout(function () { - requestSequence += 1; // increment the sequence - var requestNumber = requestSequence, // this request's sequence number - data = options.data, // ajax data function - transport = options.transport || $.ajax, - traditional = options.traditional || false, - type = options.type || 'GET'; // set type of request (GET or POST) - - data = data.call(this, query.term, query.page, query.context); - - if( null !== handler) { handler.abort(); } - - handler = transport.call(null, { - url: options.url, - dataType: options.dataType, - data: data, - type: type, - traditional: traditional, - success: function (data) { - if (requestNumber < requestSequence) { - return; - } - // TODO 3.0 - replace query.page with query so users have access to term, page, etc. - var results = options.results(data, query.page); - query.callback(results); - } - }); - }, quietMillis); - }; - } - - /** - * Produces a query function that works with a local array - * - * @param options object containing configuration parameters. The options parameter can either be an array or an - * object. - * - * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. - * - * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain - * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' - * key can either be a String in which case it is expected that each element in the 'data' array has a key with the - * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract - * the text. - */ - function local(options) { - var data = options, // data elements - dataText, - text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search - - if (!$.isArray(data)) { - text = data.text; - // if text is not a function we assume it to be a key name - if (!$.isFunction(text)) { - dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available - text = function (item) { return item[dataText]; }; - } - data = data.results; - } - - return function (query) { - var t = query.term, filtered = { results: [] }, process; - if (t === "") { - query.callback({results: data}); - return; - } - - process = function(datum, collection) { - var group, attr; - datum = datum[0]; - if (datum.children) { - group = {}; - for (attr in datum) { - if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; - } - group.children=[]; - $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); - if (group.children.length) { - collection.push(group); - } - } else { - if (query.matcher(t, text(datum))) { - collection.push(datum); - } - } - }; - - $(data).each2(function(i, datum) { process(datum, filtered.results); }); - query.callback(filtered); - }; - } - - // TODO javadoc - function tags(data) { - // TODO even for a function we should probably return a wrapper that does the same object/string check as - // the function for arrays. otherwise only functions that return objects are supported. - if ($.isFunction(data)) { - return data; - } - - // if not a function we assume it to be an array - - return function (query) { - var t = query.term, filtered = {results: []}; - $(data).each(function () { - var isObject = this.text !== undefined, - text = isObject ? this.text : this; - if (t === "" || query.matcher(t, text)) { - filtered.results.push(isObject ? this : {id: this, text: this}); - } - }); - query.callback(filtered); - }; - } - - /** - * Checks if the formatter function should be used. - * - * Throws an error if it is not a function. Returns true if it should be used, - * false if no formatting should be performed. - * - * @param formatter - */ - function checkFormatter(formatter, formatterName) { - if ($.isFunction(formatter)) return true; - if (!formatter) return false; - throw new Error("formatterName must be a function or a falsy value"); - } - - function evaluate(val) { - return $.isFunction(val) ? val() : val; - } - - function countResults(results) { - var count = 0; - $.each(results, function(i, item) { - if (item.children) { - count += countResults(item.children); - } else { - count++; - } - }); - return count; - } - - /** - * Default tokenizer. This function uses breaks the input on substring match of any string from the - * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those - * two options have to be defined in order for the tokenizer to work. - * - * @param input text user has typed so far or pasted into the search field - * @param selection currently selected choices - * @param selectCallback function(choice) callback tho add the choice to selection - * @param opts select2's opts - * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value - */ - function defaultTokenizer(input, selection, selectCallback, opts) { - var original = input, // store the original so we can compare and know if we need to tell the search to update its text - dupe = false, // check for whether a token we extracted represents a duplicate selected choice - token, // token - index, // position at which the separator was found - i, l, // looping variables - separator; // the matched separator - - if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; - - while (true) { - index = -1; - - for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { - separator = opts.tokenSeparators[i]; - index = input.indexOf(separator); - if (index >= 0) break; - } - - if (index < 0) break; // did not find any token separator in the input string, bail - - token = input.substring(0, index); - input = input.substring(index + separator.length); - - if (token.length > 0) { - token = opts.createSearchChoice(token, selection); - if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { - dupe = false; - for (i = 0, l = selection.length; i < l; i++) { - if (equal(opts.id(token), opts.id(selection[i]))) { - dupe = true; break; - } - } - - if (!dupe) selectCallback(token); - } - } - } - - if (original.localeCompare(input) != 0) return input; - } - - /** - * blurs any Select2 container that has focus when an element outside them was clicked or received focus - * - * also takes care of clicks on label tags that point to the source element - */ - $(document).ready(function () { - $(document).delegate("body", "mousedown touchend", function (e) { - var target = $(e.target).closest("div.select2-container").get(0), attr; - if (target) { - $(document).find("div.select2-container-active").each(function () { - if (this !== target) $(this).data("select2").blur(); - }); - } else { - target = $(e.target).closest("div.select2-drop").get(0); - $(document).find("div.select2-drop-active").each(function () { - if (this !== target) $(this).data("select2").blur(); - }); - } - - target=$(e.target); - attr = target.attr("for"); - if ("LABEL" === e.target.tagName && attr && attr.length > 0) { - target = $("#"+attr); - target = target.data("select2"); - if (target !== undefined) { target.focus(); e.preventDefault();} - } - }); - }); - - /** - * Creates a new class - * - * @param superClass - * @param methods - */ - function clazz(SuperClass, methods) { - var constructor = function () {}; - constructor.prototype = new SuperClass; - constructor.prototype.constructor = constructor; - constructor.prototype.parent = SuperClass.prototype; - constructor.prototype = $.extend(constructor.prototype, methods); - return constructor; - } - - AbstractSelect2 = clazz(Object, { - - // abstract - bind: function (func) { - var self = this; - return function () { - func.apply(self, arguments); - }; - }, - - // abstract - init: function (opts) { - var results, search, resultsSelector = ".select2-results"; - - // prepare options - this.opts = opts = this.prepareOpts(opts); - - this.id=opts.id; - - // destroy if called on an existing component - if (opts.element.data("select2") !== undefined && - opts.element.data("select2") !== null) { - this.destroy(); - } - - this.enabled=true; - this.container = this.createContainer(); - - this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); - this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); - this.container.attr("id", this.containerId); - - // cache the body so future lookups are cheap - this.body = thunk(function() { return opts.element.closest("body"); }); - - if (opts.element.attr("class") !== undefined) { - this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, '')); - } - - this.container.css(evaluate(opts.containerCss)); - this.container.addClass(evaluate(opts.containerCssClass)); - - // swap container for the element - this.opts.element - .data("select2", this) - .hide() - .before(this.container); - this.container.data("select2", this); - - this.dropdown = this.container.find(".select2-drop"); - this.dropdown.addClass(evaluate(opts.dropdownCssClass)); - this.dropdown.data("select2", this); - - this.results = results = this.container.find(resultsSelector); - this.search = search = this.container.find("input.select2-input"); - - search.attr("tabIndex", this.opts.element.attr("tabIndex")); - - this.resultsPage = 0; - this.context = null; - - // initialize the container - this.initContainer(); - this.initContainerWidth(); - - installFilteredMouseMove(this.results); - this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent)); - - installDebouncedScroll(80, this.results); - this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded)); - - // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel - if ($.fn.mousewheel) { - results.mousewheel(function (e, delta, deltaX, deltaY) { - var top = results.scrollTop(), height; - if (deltaY > 0 && top - deltaY <= 0) { - results.scrollTop(0); - killEvent(e); - } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { - results.scrollTop(results.get(0).scrollHeight - results.height()); - killEvent(e); - } - }); - } - - installKeyUpChangeEvent(search); - search.bind("keyup-change", this.bind(this.updateResults)); - search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); }); - search.bind("blur", function () { search.removeClass("select2-focused");}); - - this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) { - if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) { - this.highlightUnderEvent(e); - this.selectHighlighted(e); - } else { - this.focusSearch(); - } - killEvent(e); - })); - - // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening - // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's - // dom it will trigger the popup close, which is not what we want - this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); }); - - if ($.isFunction(this.opts.initSelection)) { - // initialize selection based on the current value of the source element - this.initSelection(); - - // if the user has provided a function that can set selection based on the value of the source element - // we monitor the change event on the element and trigger it, allowing for two way synchronization - this.monitorSource(); - } - - if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable(); - }, - - // abstract - destroy: function () { - var select2 = this.opts.element.data("select2"); - if (select2 !== undefined) { - select2.container.remove(); - select2.dropdown.remove(); - select2.opts.element - .removeData("select2") - .unbind(".select2") - .show(); - } - }, - - // abstract - prepareOpts: function (opts) { - var element, select, idKey, ajaxUrl; - - element = opts.element; - - if (element.get(0).tagName.toLowerCase() === "select") { - this.select = select = opts.element; - } - - if (select) { - // these options are not allowed when attached to a select because they are picked up off the element itself - $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { - if (this in opts) { - throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element."); - } - }); - } - - opts = $.extend({}, { - populateResults: function(container, results, query) { - var populate, data, result, children, id=this.opts.id, self=this; - - populate=function(results, container, depth) { - - var i, l, result, selectable, compound, node, label, innerContainer, formatted; - for (i = 0, l = results.length; i < l; i = i + 1) { - - result=results[i]; - selectable=id(result) !== undefined; - compound=result.children && result.children.length > 0; - - node=$("<li></li>"); - node.addClass("select2-results-dept-"+depth); - node.addClass("select2-result"); - node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable"); - if (compound) { node.addClass("select2-result-with-children"); } - node.addClass(self.opts.formatResultCssClass(result)); - - label=$("<div></div>"); - label.addClass("select2-result-label"); - - formatted=opts.formatResult(result, label, query); - if (formatted!==undefined) { - label.html(self.opts.escapeMarkup(formatted)); - } - - node.append(label); - - if (compound) { - - innerContainer=$("<ul></ul>"); - innerContainer.addClass("select2-result-sub"); - populate(result.children, innerContainer, depth+1); - node.append(innerContainer); - } - - node.data("select2-data", result); - container.append(node); - } - }; - - populate(results, container, 0); - } - }, $.fn.select2.defaults, opts); - - if (typeof(opts.id) !== "function") { - idKey = opts.id; - opts.id = function (e) { return e[idKey]; }; - } - - if (select) { - opts.query = this.bind(function (query) { - var data = { results: [], more: false }, - term = query.term, - children, firstChild, process; - - process=function(element, collection) { - var group; - if (element.is("option")) { - if (query.matcher(term, element.text(), element)) { - collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")}); - } - } else if (element.is("optgroup")) { - group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")}; - element.children().each2(function(i, elm) { process(elm, group.children); }); - if (group.children.length>0) { - collection.push(group); - } - } - }; - - children=element.children(); - - // ignore the placeholder option if there is one - if (this.getPlaceholder() !== undefined && children.length > 0) { - firstChild = children[0]; - if ($(firstChild).text() === "") { - children=children.not(firstChild); - } - } - - children.each2(function(i, elm) { process(elm, data.results); }); - - query.callback(data); - }); - // this is needed because inside val() we construct choices from options and there id is hardcoded - opts.id=function(e) { return e.id; }; - opts.formatResultCssClass = function(data) { return data.css; } - } else { - if (!("query" in opts)) { - if ("ajax" in opts) { - ajaxUrl = opts.element.data("ajax-url"); - if (ajaxUrl && ajaxUrl.length > 0) { - opts.ajax.url = ajaxUrl; - } - opts.query = ajax(opts.ajax); - } else if ("data" in opts) { - opts.query = local(opts.data); - } else if ("tags" in opts) { - opts.query = tags(opts.tags); - opts.createSearchChoice = function (term) { return {id: term, text: term}; }; - opts.initSelection = function (element, callback) { - var data = []; - $(splitVal(element.val(), opts.separator)).each(function () { - var id = this, text = this, tags=opts.tags; - if ($.isFunction(tags)) tags=tags(); - $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } }); - data.push({id: id, text: text}); - }); - - callback(data); - }; - } - } - } - if (typeof(opts.query) !== "function") { - throw "query function not defined for Select2 " + opts.element.attr("id"); - } - - return opts; - }, - - /** - * Monitor the original element for changes and update select2 accordingly - */ - // abstract - monitorSource: function () { - this.opts.element.bind("change.select2", this.bind(function (e) { - if (this.opts.element.data("select2-change-triggered") !== true) { - this.initSelection(); - } - })); - }, - - /** - * Triggers the change event on the source element - */ - // abstract - triggerChange: function (details) { - - details = details || {}; - details= $.extend({}, details, { type: "change", val: this.val() }); - // prevents recursive triggering - this.opts.element.data("select2-change-triggered", true); - this.opts.element.trigger(details); - this.opts.element.data("select2-change-triggered", false); - - // some validation frameworks ignore the change event and listen instead to keyup, click for selects - // so here we trigger the click event manually - this.opts.element.click(); - - // ValidationEngine ignorea the change event and listens instead to blur - // so here we trigger the blur event manually if so desired - if (this.opts.blurOnChange) - this.opts.element.blur(); - }, - - - // abstract - enable: function() { - if (this.enabled) return; - - this.enabled=true; - this.container.removeClass("select2-container-disabled"); - }, - - // abstract - disable: function() { - if (!this.enabled) return; - - this.close(); - - this.enabled=false; - this.container.addClass("select2-container-disabled"); - }, - - // abstract - opened: function () { - return this.container.hasClass("select2-dropdown-open"); - }, - - // abstract - positionDropdown: function() { - var offset = this.container.offset(), - height = this.container.outerHeight(), - width = this.container.outerWidth(), - dropHeight = this.dropdown.outerHeight(), - viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight, - dropTop = offset.top + height, - dropLeft = offset.left, - enoughRoomBelow = dropTop + dropHeight <= viewportBottom, - enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(), - aboveNow = this.dropdown.hasClass("select2-drop-above"), - bodyOffset, - above, - css; - - // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow); - // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove); - - // fix positioning when body has an offset and is not position: static - - if (this.body().css('position') !== 'static') { - bodyOffset = this.body().offset(); - dropTop -= bodyOffset.top; - dropLeft -= bodyOffset.left; - } - - // always prefer the current above/below alignment, unless there is not enough room - - if (aboveNow) { - above = true; - if (!enoughRoomAbove && enoughRoomBelow) above = false; - } else { - above = false; - if (!enoughRoomBelow && enoughRoomAbove) above = true; - } - - if (above) { - dropTop = offset.top - dropHeight; - this.container.addClass("select2-drop-above"); - this.dropdown.addClass("select2-drop-above"); - } - else { - this.container.removeClass("select2-drop-above"); - this.dropdown.removeClass("select2-drop-above"); - } - - css = $.extend({ - top: dropTop, - left: dropLeft, - width: width - }, evaluate(this.opts.dropdownCss)); - - this.dropdown.css(css); - }, - - // abstract - shouldOpen: function() { - var event; - - if (this.opened()) return false; - - event = $.Event("open"); - this.opts.element.trigger(event); - return !event.isDefaultPrevented(); - }, - - // abstract - clearDropdownAlignmentPreference: function() { - // clear the classes used to figure out the preference of where the dropdown should be opened - this.container.removeClass("select2-drop-above"); - this.dropdown.removeClass("select2-drop-above"); - }, - - /** - * Opens the dropdown - * - * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example, - * the dropdown is already open, or if the 'open' event listener on the element called preventDefault(). - */ - // abstract - open: function () { - - if (!this.shouldOpen()) return false; - - window.setTimeout(this.bind(this.opening), 1); - - return true; - }, - - /** - * Performs the opening of the dropdown - */ - // abstract - opening: function() { - var cid = this.containerId, selector = this.containerSelector, - scroll = "scroll." + cid, resize = "resize." + cid; - - setTimeout(function () { - this.container.parents().each(function() { - $(this).bind(scroll, function() { - var s2 = $(selector); - if (s2.length == 0) { - $(this).unbind(scroll); - } - s2.select2("close"); - }); - }); - }.bind(this), 100); - - $(window).bind(resize, function() { - var s2 = $(selector); - if (s2.length == 0) { - $(window).unbind(resize); - } - s2.select2("close"); - }); - - this.clearDropdownAlignmentPreference(); - - if (this.search.val() === " ") { this.search.val(""); } - - this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); - - this.updateResults(true); - - if(this.dropdown[0] !== this.body().children().last()[0]) { - this.dropdown.detach().appendTo(this.body()); - } - - this.dropdown.show(); - - this.positionDropdown(); - this.dropdown.addClass("select2-drop-active"); - - this.ensureHighlightVisible(); - - this.focusSearch(); - }, - - // abstract - close: function () { - if (!this.opened()) return; - - var self = this; - - this.container.parents().each(function() { - $(this).unbind("scroll." + self.containerId); - }); - $(window).unbind("resize." + this.containerId); - - this.clearDropdownAlignmentPreference(); - - this.dropdown.hide(); - this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active"); - this.results.empty(); - this.clearSearch(); - - this.opts.element.trigger($.Event("close")); - }, - - // abstract - clearSearch: function () { - - }, - - // abstract - ensureHighlightVisible: function () { - var results = this.results, children, index, child, hb, rb, y, more; - - index = this.highlight(); - - if (index < 0) return; - - if (index == 0) { - - // if the first element is highlighted scroll all the way to the top, - // that way any unselectable headers above it will also be scrolled - // into view - - results.scrollTop(0); - return; - } - - children = results.find(".select2-result-selectable"); - - child = $(children[index]); - - hb = child.offset().top + child.outerHeight(); - - // if this is the last child lets also make sure select2-more-results is visible - if (index === children.length - 1) { - more = results.find("li.select2-more-results"); - if (more.length > 0) { - hb = more.offset().top + more.outerHeight(); - } - } - - rb = results.offset().top + results.outerHeight(); - if (hb > rb) { - results.scrollTop(results.scrollTop() + (hb - rb)); - } - y = child.offset().top - results.offset().top; - - // make sure the top of the element is visible - if (y < 0) { - results.scrollTop(results.scrollTop() + y); // y is negative - } - }, - - // abstract - moveHighlight: function (delta) { - var choices = this.results.find(".select2-result-selectable"), - index = this.highlight(); - - while (index > -1 && index < choices.length) { - index += delta; - var choice = $(choices[index]); - if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) { - this.highlight(index); - break; - } - } - }, - - // abstract - highlight: function (index) { - var choices = this.results.find(".select2-result-selectable").not(".select2-disabled"); - - if (arguments.length === 0) { - return indexOf(choices.filter(".select2-highlighted")[0], choices.get()); - } - - if (index >= choices.length) index = choices.length - 1; - if (index < 0) index = 0; - - choices.removeClass("select2-highlighted"); - - $(choices[index]).addClass("select2-highlighted"); - this.ensureHighlightVisible(); - - }, - - // abstract - countSelectableResults: function() { - return this.results.find(".select2-result-selectable").not(".select2-disabled").length; - }, - - // abstract - highlightUnderEvent: function (event) { - var el = $(event.target).closest(".select2-result-selectable"); - if (el.length > 0 && !el.is(".select2-highlighted")) { - var choices = this.results.find('.select2-result-selectable'); - this.highlight(choices.index(el)); - } else if (el.length == 0) { - // if we are over an unselectable item remove al highlights - this.results.find(".select2-highlighted").removeClass("select2-highlighted"); - } - }, - - // abstract - loadMoreIfNeeded: function () { - var results = this.results, - more = results.find("li.select2-more-results"), - below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible - offset = -1, // index of first element without data - page = this.resultsPage + 1, - self=this, - term=this.search.val(), - context=this.context; - - if (more.length === 0) return; - below = more.offset().top - results.offset().top - results.height(); - - if (below <= 0) { - more.addClass("select2-active"); - this.opts.query({ - term: term, - page: page, - context: context, - matcher: this.opts.matcher, - callback: this.bind(function (data) { - - // ignore a response if the select2 has been closed before it was received - if (!self.opened()) return; - - - self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context}); - - if (data.more===true) { - more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1)); - window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); - } else { - more.remove(); - } - self.positionDropdown(); - self.resultsPage = page; - })}); - } - }, - - /** - * Default tokenizer function which does nothing - */ - tokenize: function() { - - }, - - /** - * @param initial whether or not this is the call to this method right after the dropdown has been opened - */ - // abstract - updateResults: function (initial) { - var search = this.search, results = this.results, opts = this.opts, data, self=this, input; - - // if the search is currently hidden we do not alter the results - if (initial !== true && (this.showSearchInput === false || !this.opened())) { - return; - } - - search.addClass("select2-active"); - - function postRender() { - results.scrollTop(0); - search.removeClass("select2-active"); - self.positionDropdown(); - } - - function render(html) { - results.html(self.opts.escapeMarkup(html)); - postRender(); - } - - if (opts.maximumSelectionSize >=1) { - data = this.data(); - if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) { - render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>"); - return; - } - } - - if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) { - render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>"); - return; - } - else { - render("<li class='select2-searching'>" + opts.formatSearching() + "</li>"); - } - - // give the tokenizer a chance to pre-process the input - input = this.tokenize(); - if (input != undefined && input != null) { - search.val(input); - } - - this.resultsPage = 1; - opts.query({ - term: search.val(), - page: this.resultsPage, - context: null, - matcher: opts.matcher, - callback: this.bind(function (data) { - var def; // default choice - - // ignore a response if the select2 has been closed before it was received - if (!this.opened()) return; - - // save context, if any - this.context = (data.context===undefined) ? null : data.context; - - // create a default choice and prepend it to the list - if (this.opts.createSearchChoice && search.val() !== "") { - def = this.opts.createSearchChoice.call(null, search.val(), data.results); - if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) { - if ($(data.results).filter( - function () { - return equal(self.id(this), self.id(def)); - }).length === 0) { - data.results.unshift(def); - } - } - } - - if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) { - render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>"); - return; - } - - results.empty(); - self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null}); - - if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) { - results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>"); - window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); - } - - this.postprocessResults(data, initial); - - postRender(); - })}); - }, - - // abstract - cancel: function () { - this.close(); - }, - - // abstract - blur: function () { - this.close(); - this.container.removeClass("select2-container-active"); - this.dropdown.removeClass("select2-drop-active"); - // synonymous to .is(':focus'), which is available in jquery >= 1.6 - if (this.search[0] === document.activeElement) { this.search.blur(); } - this.clearSearch(); - this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); - }, - - // abstract - focusSearch: function () { - // need to do it here as well as in timeout so it works in IE - this.search.show(); - this.search.focus(); - - /* we do this in a timeout so that current event processing can complete before this code is executed. - this makes sure the search field is focussed even if the current event would blur it */ - window.setTimeout(this.bind(function () { - // reset the value so IE places the cursor at the end of the input box - this.search.show(); - this.search.focus(); - this.search.val(this.search.val()); - }), 10); - }, - - // abstract - selectHighlighted: function () { - var index=this.highlight(), - highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"), - data = highlighted.closest('.select2-result-selectable').data("select2-data"); - if (data) { - highlighted.addClass("select2-disabled"); - this.highlight(index); - this.onSelect(data); - } - }, - - // abstract - getPlaceholder: function () { - return this.opts.element.attr("placeholder") || - this.opts.element.attr("data-placeholder") || // jquery 1.4 compat - this.opts.element.data("placeholder") || - this.opts.placeholder; - }, - - /** - * Get the desired width for the container element. This is - * derived first from option `width` passed to select2, then - * the inline 'style' on the original element, and finally - * falls back to the jQuery calculated element width. - */ - // abstract - initContainerWidth: function () { - function resolveContainerWidth() { - var style, attrs, matches, i, l; - - if (this.opts.width === "off") { - return null; - } else if (this.opts.width === "element"){ - return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px'; - } else if (this.opts.width === "copy" || this.opts.width === "resolve") { - // check if there is inline style on the element that contains width - style = this.opts.element.attr('style'); - if (style !== undefined) { - attrs = style.split(';'); - for (i = 0, l = attrs.length; i < l; i = i + 1) { - matches = attrs[i].replace(/\s/g, '') - .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/); - if (matches !== null && matches.length >= 1) - return matches[1]; - } - } - - if (this.opts.width === "resolve") { - // next check if css('width') can resolve a width that is percent based, this is sometimes possible - // when attached to input type=hidden or elements hidden via css - style = this.opts.element.css('width'); - if (style.indexOf("%") > 0) return style; - - // finally, fallback on the calculated width of the element - return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px'); - } - - return null; - } else if ($.isFunction(this.opts.width)) { - return this.opts.width(); - } else { - return this.opts.width; - } - }; - - var width = resolveContainerWidth.call(this); - if (width !== null) { - this.container.attr("style", "width: "+width); - } - } - }); - - SingleSelect2 = clazz(AbstractSelect2, { - - // single - - createContainer: function () { - var container = $("<div></div>", { - "class": "select2-container" - }).html([ - " <a href='#' onclick='return false;' class='select2-choice'>", - " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>", - " <div><b></b></div>" , - "</a>", - " <div class='select2-drop select2-offscreen'>" , - " <div class='select2-search'>" , - " <input type='text' autocomplete='off' class='select2-input'/>" , - " </div>" , - " <ul class='select2-results'>" , - " </ul>" , - "</div>"].join("")); - return container; - }, - - // single - opening: function () { - this.search.show(); - this.parent.opening.apply(this, arguments); - this.dropdown.removeClass("select2-offscreen"); - }, - - // single - close: function () { - if (!this.opened()) return; - console.log('closing...'); - this.parent.close.apply(this, arguments); - this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show(); - }, - - // single - focus: function () { - this.close(); - this.selection.focus(); - }, - - // single - isFocused: function () { - return this.selection[0] === document.activeElement; - }, - - // single - cancel: function () { - this.parent.cancel.apply(this, arguments); - this.selection.focus(); - }, - - // single - initContainer: function () { - - var selection, - container = this.container, - dropdown = this.dropdown, - clickingInside = false; - - this.selection = selection = container.find(".select2-choice"); - - this.search.bind("keydown", this.bind(function (e) { - if (!this.enabled) return; - - if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { - // prevent the page from scrolling - killEvent(e); - return; - } - - if (this.opened()) { - switch (e.which) { - case KEY.UP: - case KEY.DOWN: - this.moveHighlight((e.which === KEY.UP) ? -1 : 1); - killEvent(e); - return; - case KEY.TAB: - case KEY.ENTER: - this.selectHighlighted(); - killEvent(e); - return; - case KEY.ESC: - this.cancel(e); - killEvent(e); - return; - } - } else { - - if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { - return; - } - - if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { - return; - } - - this.open(); - - if (e.which === KEY.ENTER) { - // do not propagate the event otherwise we open, and propagate enter which closes - return; - } - } - })); - - this.search.bind("focus", this.bind(function() { - this.selection.attr("tabIndex", "-1"); - })); - this.search.bind("blur", this.bind(function() { - if (!this.opened()) this.container.removeClass("select2-container-active"); - window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10); - })); - - selection.bind("mousedown", this.bind(function (e) { - clickingInside = true; - - if (this.opened()) { - this.close(); - this.selection.focus(); - } else if (this.enabled) { - this.open(); - } - - clickingInside = false; - })); - - dropdown.bind("mousedown", this.bind(function() { this.search.focus(); })); - - selection.bind("focus", this.bind(function() { - this.container.addClass("select2-container-active"); - // hide the search so the tab key does not focus on it - this.search.attr("tabIndex", "-1"); - })); - - selection.bind("blur", this.bind(function() { - if (!this.opened()) { - this.container.removeClass("select2-container-active"); - } - window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10); - })); - - selection.bind("keydown", this.bind(function(e) { - if (!this.enabled) return; - - if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { - // prevent the page from scrolling - killEvent(e); - return; - } - - if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) - || e.which === KEY.ESC) { - return; - } - - if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { - return; - } - - if (e.which == KEY.DELETE) { - if (this.opts.allowClear) { - this.clear(); - } - return; - } - - this.open(); - - if (e.which === KEY.ENTER) { - // do not propagate the event otherwise we open, and propagate enter which closes - killEvent(e); - return; - } - - // do not set the search input value for non-alpha-numeric keys - // otherwise pressing down results in a '(' being set in the search field - if (e.which < 48 ) { // '0' == 48 - killEvent(e); - return; - } - - var keyWritten = String.fromCharCode(e.which).toLowerCase(); - - if (e.shiftKey) { - keyWritten = keyWritten.toUpperCase(); - } - - // focus the field before calling val so the cursor ends up after the value instead of before - this.search.focus(); - this.search.val(keyWritten); - - // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry - killEvent(e); - })); - - selection.delegate("abbr", "mousedown", this.bind(function (e) { - if (!this.enabled) return; - this.clear(); - killEvent(e); - this.close(); - this.triggerChange(); - this.selection.focus(); - })); - - this.setPlaceholder(); - - this.search.bind("focus", this.bind(function() { - this.container.addClass("select2-container-active"); - })); - }, - - // single - clear: function() { - this.opts.element.val(""); - this.selection.find("span").empty(); - this.selection.removeData("select2-data"); - this.setPlaceholder(); - }, - - /** - * Sets selection based on source element's value - */ - // single - initSelection: function () { - var selected; - if (this.opts.element.val() === "") { - this.close(); - this.setPlaceholder(); - } else { - var self = this; - this.opts.initSelection.call(null, this.opts.element, function(selected){ - if (selected !== undefined && selected !== null) { - self.updateSelection(selected); - self.close(); - self.setPlaceholder(); - } - }); - } - }, - - // single - prepareOpts: function () { - var opts = this.parent.prepareOpts.apply(this, arguments); - - if (opts.element.get(0).tagName.toLowerCase() === "select") { - // install the selection initializer - opts.initSelection = function (element, callback) { - var selected = element.find(":selected"); - // a single select box always has a value, no need to null check 'selected' - if ($.isFunction(callback)) - callback({id: selected.attr("value"), text: selected.text()}); - }; - } - - return opts; - }, - - // single - setPlaceholder: function () { - var placeholder = this.getPlaceholder(); - - if (this.opts.element.val() === "" && placeholder !== undefined) { - - // check for a first blank option if attached to a select - if (this.select && this.select.find("option:first").text() !== "") return; - - this.selection.find("span").html(this.opts.escapeMarkup(placeholder)); - - this.selection.addClass("select2-default"); - - this.selection.find("abbr").hide(); - } - }, - - // single - postprocessResults: function (data, initial) { - var selected = 0, self = this, showSearchInput = true; - - // find the selected element in the result list - - this.results.find(".select2-result-selectable").each2(function (i, elm) { - if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { - selected = i; - return false; - } - }); - - // and highlight it - - this.highlight(selected); - - // hide the search box if this is the first we got the results and there are a few of them - - if (initial === true) { - showSearchInput = this.showSearchInput = countResults(data.results) >= this.opts.minimumResultsForSearch; - this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden"); - - //add "select2-with-searchbox" to the container if search box is shown - $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox"); - } - - }, - - // single - onSelect: function (data) { - var old = this.opts.element.val(); - - this.opts.element.val(this.id(data)); - this.updateSelection(data); - this.close(); - this.selection.focus(); - - if (!equal(old, this.id(data))) { this.triggerChange(); } - }, - - // single - updateSelection: function (data) { - - var container=this.selection.find("span"), formatted; - - this.selection.data("select2-data", data); - - container.empty(); - formatted=this.opts.formatSelection(data, container); - if (formatted !== undefined) { - container.append(this.opts.escapeMarkup(formatted)); - } - - this.selection.removeClass("select2-default"); - - if (this.opts.allowClear && this.getPlaceholder() !== undefined) { - this.selection.find("abbr").show(); - } - }, - - // single - val: function () { - var val, data = null, self = this; - - if (arguments.length === 0) { - return this.opts.element.val(); - } - - val = arguments[0]; - - if (this.select) { - this.select - .val(val) - .find(":selected").each2(function (i, elm) { - data = {id: elm.attr("value"), text: elm.text()}; - return false; - }); - this.updateSelection(data); - this.setPlaceholder(); - } else { - if (this.opts.initSelection === undefined) { - throw new Error("cannot call val() if initSelection() is not defined"); - } - // val is an id. !val is true for [undefined,null,''] - if (!val) { - this.clear(); - return; - } - this.opts.element.val(val); - this.opts.initSelection(this.opts.element, function(data){ - self.opts.element.val(!data ? "" : self.id(data)); - self.updateSelection(data); - self.setPlaceholder(); - }); - } - }, - - // single - clearSearch: function () { - this.search.val(""); - }, - - // single - data: function(value) { - var data; - - if (arguments.length === 0) { - data = this.selection.data("select2-data"); - if (data == undefined) data = null; - return data; - } else { - if (!value || value === "") { - this.clear(); - } else { - this.opts.element.val(!value ? "" : this.id(value)); - this.updateSelection(value); - } - } - } - }); - - MultiSelect2 = clazz(AbstractSelect2, { - - // multi - createContainer: function () { - var container = $("<div></div>", { - "class": "select2-container select2-container-multi" - }).html([ - " <ul class='select2-choices'>", - //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" , - " <li class='select2-search-field'>" , - " <input type='text' autocomplete='off' class='select2-input'>" , - " </li>" , - "</ul>" , - "<div class='select2-drop select2-drop-multi' style='display:none;'>" , - " <ul class='select2-results'>" , - " </ul>" , - "</div>"].join("")); - return container; - }, - - // multi - prepareOpts: function () { - var opts = this.parent.prepareOpts.apply(this, arguments); - - // TODO validate placeholder is a string if specified - - if (opts.element.get(0).tagName.toLowerCase() === "select") { - // install sthe selection initializer - opts.initSelection = function (element,callback) { - - var data = []; - element.find(":selected").each2(function (i, elm) { - data.push({id: elm.attr("value"), text: elm.text()}); - }); - - if ($.isFunction(callback)) - callback(data); - }; - } - - return opts; - }, - - // multi - initContainer: function () { - - var selector = ".select2-choices", selection; - - this.searchContainer = this.container.find(".select2-search-field"); - this.selection = selection = this.container.find(selector); - - this.search.bind("keydown", this.bind(function (e) { - if (!this.enabled) return; - - if (e.which === KEY.BACKSPACE && this.search.val() === "") { - this.close(); - - var choices, - selected = selection.find(".select2-search-choice-focus"); - if (selected.length > 0) { - this.unselect(selected.first()); - this.search.width(10); - killEvent(e); - return; - } - - choices = selection.find(".select2-search-choice"); - if (choices.length > 0) { - choices.last().addClass("select2-search-choice-focus"); - } - } else { - selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); - } - - if (this.opened()) { - switch (e.which) { - case KEY.UP: - case KEY.DOWN: - this.moveHighlight((e.which === KEY.UP) ? -1 : 1); - killEvent(e); - return; - case KEY.ENTER: - case KEY.TAB: - this.selectHighlighted(); - killEvent(e); - return; - case KEY.ESC: - this.cancel(e); - killEvent(e); - return; - } - } - - if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) - || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { - return; - } - - if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { - return; - } - - this.open(); - - if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { - // prevent the page from scrolling - killEvent(e); - } - })); - - this.search.bind("keyup", this.bind(this.resizeSearch)); - - this.search.bind("blur", this.bind(function(e) { - this.container.removeClass("select2-container-active"); - this.search.removeClass("select2-focused"); - this.clearSearch(); - e.stopImmediatePropagation(); - })); - - this.container.delegate(selector, "mousedown", this.bind(function (e) { - if (!this.enabled) return; - if ($(e.target).closest(".select2-search-choice").length > 0) { - // clicked inside a select2 search choice, do not open - return; - } - this.clearPlaceholder(); - this.open(); - this.focusSearch(); - e.preventDefault(); - })); - - this.container.delegate(selector, "focus", this.bind(function () { - if (!this.enabled) return; - this.container.addClass("select2-container-active"); - this.dropdown.addClass("select2-drop-active"); - this.clearPlaceholder(); - })); - - // set the placeholder if necessary - this.clearSearch(); - }, - - // multi - enable: function() { - if (this.enabled) return; - - this.parent.enable.apply(this, arguments); - - this.search.removeAttr("disabled"); - }, - - // multi - disable: function() { - if (!this.enabled) return; - - this.parent.disable.apply(this, arguments); - - this.search.attr("disabled", true); - }, - - // multi - initSelection: function () { - var data; - if (this.opts.element.val() === "") { - this.updateSelection([]); - this.close(); - // set the placeholder if necessary - this.clearSearch(); - } - if (this.select || this.opts.element.val() !== "") { - var self = this; - this.opts.initSelection.call(null, this.opts.element, function(data){ - if (data !== undefined && data !== null) { - self.updateSelection(data); - self.close(); - // set the placeholder if necessary - self.clearSearch(); - } - }); - } - }, - - // multi - clearSearch: function () { - var placeholder = this.getPlaceholder(); - - if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { - this.search.val(placeholder).addClass("select2-default"); - // stretch the search box to full width of the container so as much of the placeholder is visible as possible - this.resizeSearch(); - } else { - // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug - // that does not properly render the caret when the field starts out blank - this.search.val(" ").width(10); - } - }, - - // multi - clearPlaceholder: function () { - if (this.search.hasClass("select2-default")) { - this.search.val("").removeClass("select2-default"); - } else { - // work around for the space character we set to avoid firefox caret bug - if (this.search.val() === " ") this.search.val(""); - } - }, - - // multi - opening: function () { - this.parent.opening.apply(this, arguments); - - this.clearPlaceholder(); - this.resizeSearch(); - this.focusSearch(); - }, - - // multi - close: function () { - if (!this.opened()) return; - this.parent.close.apply(this, arguments); - }, - - // multi - focus: function () { - this.close(); - this.search.focus(); - }, - - // multi - isFocused: function () { - return this.search.hasClass("select2-focused"); - }, - - // multi - updateSelection: function (data) { - var ids = [], filtered = [], self = this; - - // filter out duplicates - $(data).each(function () { - if (indexOf(self.id(this), ids) < 0) { - ids.push(self.id(this)); - filtered.push(this); - } - }); - data = filtered; - - this.selection.find(".select2-search-choice").remove(); - $(data).each(function () { - self.addSelectedChoice(this); - }); - self.postprocessResults(); - }, - - tokenize: function() { - var input = this.search.val(); - input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts); - if (input != null && input != undefined) { - this.search.val(input); - if (input.length > 0) { - this.open(); - } - } - - }, - - // multi - onSelect: function (data) { - this.addSelectedChoice(data); - if (this.select) { this.postprocessResults(); } - - if (this.opts.closeOnSelect) { - this.close(); - this.search.width(10); - } else { - if (this.countSelectableResults()>0) { - this.search.width(10); - this.resizeSearch(); - this.positionDropdown(); - } else { - // if nothing left to select close - this.close(); - } - } - - // since its not possible to select an element that has already been - // added we do not need to check if this is a new element before firing change - this.triggerChange({ added: data }); - - this.focusSearch(); - }, - - // multi - cancel: function () { - this.close(); - this.focusSearch(); - }, - - // multi - addSelectedChoice: function (data) { - var choice=$( - "<li class='select2-search-choice'>" + - " <div></div>" + - " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" + - "</li>"), - id = this.id(data), - val = this.getVal(), - formatted; - - formatted=this.opts.formatSelection(data, choice); - choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>"); - choice.find(".select2-search-choice-close") - .bind("mousedown", killEvent) - .bind("click dblclick", this.bind(function (e) { - if (!this.enabled) return; - - $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){ - this.unselect($(e.target)); - this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); - this.close(); - this.focusSearch(); - })).dequeue(); - killEvent(e); - })).bind("focus", this.bind(function () { - if (!this.enabled) return; - this.container.addClass("select2-container-active"); - this.dropdown.addClass("select2-drop-active"); - })); - - choice.data("select2-data", data); - choice.insertBefore(this.searchContainer); - - val.push(id); - this.setVal(val); - }, - - // multi - unselect: function (selected) { - var val = this.getVal(), - data, - index; - - selected = selected.closest(".select2-search-choice"); - - if (selected.length === 0) { - throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; - } - - data = selected.data("select2-data"); - - index = indexOf(this.id(data), val); - - if (index >= 0) { - val.splice(index, 1); - this.setVal(val); - if (this.select) this.postprocessResults(); - } - selected.remove(); - this.triggerChange({ removed: data }); - }, - - // multi - postprocessResults: function () { - var val = this.getVal(), - choices = this.results.find(".select2-result-selectable"), - compound = this.results.find(".select2-result-with-children"), - self = this; - - choices.each2(function (i, choice) { - var id = self.id(choice.data("select2-data")); - if (indexOf(id, val) >= 0) { - choice.addClass("select2-disabled").removeClass("select2-result-selectable"); - } else { - choice.removeClass("select2-disabled").addClass("select2-result-selectable"); - } - }); - - compound.each2(function(i, e) { - if (e.find(".select2-result-selectable").length==0) { - e.addClass("select2-disabled"); - } else { - e.removeClass("select2-disabled"); - } - }); - - choices.each2(function (i, choice) { - if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) { - self.highlight(0); - return false; - } - }); - - }, - - // multi - resizeSearch: function () { - - var minimumWidth, left, maxWidth, containerLeft, searchWidth, - sideBorderPadding = getSideBorderPadding(this.search); - - minimumWidth = measureTextWidth(this.search) + 10; - - left = this.search.offset().left; - - maxWidth = this.selection.width(); - containerLeft = this.selection.offset().left; - - searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; - if (searchWidth < minimumWidth) { - searchWidth = maxWidth - sideBorderPadding; - } - - if (searchWidth < 40) { - searchWidth = maxWidth - sideBorderPadding; - } - this.search.width(searchWidth); - }, - - // multi - getVal: function () { - var val; - if (this.select) { - val = this.select.val(); - return val === null ? [] : val; - } else { - val = this.opts.element.val(); - return splitVal(val, this.opts.separator); - } - }, - - // multi - setVal: function (val) { - var unique; - if (this.select) { - this.select.val(val); - } else { - unique = []; - // filter out duplicates - $(val).each(function () { - if (indexOf(this, unique) < 0) unique.push(this); - }); - this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); - } - }, - - // multi - val: function () { - var val, data = [], self=this; - - if (arguments.length === 0) { - return this.getVal(); - } - - val = arguments[0]; - - if (!val) { - this.opts.element.val(""); - this.updateSelection([]); - this.clearSearch(); - return; - } - - // val is a list of ids - this.setVal(val); - - if (this.select) { - this.select.find(":selected").each(function () { - data.push({id: $(this).attr("value"), text: $(this).text()}); - }); - this.updateSelection(data); - } else { - if (this.opts.initSelection === undefined) { - throw new Error("val() cannot be called if initSelection() is not defined") - } - - this.opts.initSelection(this.opts.element, function(data){ - var ids=$(data).map(self.id); - self.setVal(ids); - self.updateSelection(data); - self.clearSearch(); - }); - } - this.clearSearch(); - }, - - // multi - onSortStart: function() { - if (this.select) { - throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead."); - } - - // collapse search field into 0 width so its container can be collapsed as well - this.search.width(0); - // hide the container - this.searchContainer.hide(); - }, - - // multi - onSortEnd:function() { - - var val=[], self=this; - - // show search and move it to the end of the list - this.searchContainer.show(); - // make sure the search container is the last item in the list - this.searchContainer.appendTo(this.searchContainer.parent()); - // since we collapsed the width in dragStarted, we resize it here - this.resizeSearch(); - - // update selection - - this.selection.find(".select2-search-choice").each(function() { - val.push(self.opts.id($(this).data("select2-data"))); - }); - this.setVal(val); - this.triggerChange(); - }, - - // multi - data: function(values) { - var self=this, ids; - if (arguments.length === 0) { - return this.selection - .find(".select2-search-choice") - .map(function() { return $(this).data("select2-data"); }) - .get(); - } else { - if (!values) { values = []; } - ids = $.map(values, function(e) { return self.opts.id(e)}); - this.setVal(ids); - this.updateSelection(values); - this.clearSearch(); - } - } - }); - - $.fn.select2 = function () { - - var args = Array.prototype.slice.call(arguments, 0), - opts, - select2, - value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"]; - - this.each(function () { - if (args.length === 0 || typeof(args[0]) === "object") { - opts = args.length === 0 ? {} : $.extend({}, args[0]); - opts.element = $(this); - - if (opts.element.get(0).tagName.toLowerCase() === "select") { - multiple = opts.element.attr("multiple"); - } else { - multiple = opts.multiple || false; - if ("tags" in opts) {opts.multiple = multiple = true;} - } - - select2 = multiple ? new MultiSelect2() : new SingleSelect2(); - select2.init(opts); - } else if (typeof(args[0]) === "string") { - - if (indexOf(args[0], allowedMethods) < 0) { - throw "Unknown method: " + args[0]; - } - - value = undefined; - select2 = $(this).data("select2"); - if (select2 === undefined) return; - if (args[0] === "container") { - value=select2.container; - } else { - value = select2[args[0]].apply(select2, args.slice(1)); - } - if (value !== undefined) {return false;} - } else { - throw "Invalid arguments to select2 plugin: " + args; - } - }); - return (value === undefined) ? this : value; - }; - - // plugin defaults, accessible to users - $.fn.select2.defaults = { - width: "copy", - closeOnSelect: true, - openOnEnter: true, - containerCss: {}, - dropdownCss: {}, - containerCssClass: "", - dropdownCssClass: "", - formatResult: function(result, container, query) { - var markup=[]; - markMatch(result.text, query.term, markup); - return markup.join(""); - }, - formatSelection: function (data, container) { - return data ? data.text : undefined; - }, - formatResultCssClass: function(data) {return undefined;}, - formatNoMatches: function () { return "No matches found"; }, - formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; }, - formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "Loading more results..."; }, - formatSearching: function () { return "Searching..."; }, - minimumResultsForSearch: 0, - minimumInputLength: 0, - maximumSelectionSize: 0, - id: function (e) { return e.id; }, - matcher: function(term, text) { - return text.toUpperCase().indexOf(term.toUpperCase()) >= 0; - }, - separator: ",", - tokenSeparators: [], - tokenizer: defaultTokenizer, - escapeMarkup: function (markup) { - if (markup && typeof(markup) === "string") { - return markup.replace(/&/g, "&"); - } - return markup; - }, - blurOnChange: false - }; - - // exports - window.Select2 = { - query: { - ajax: ajax, - local: local, - tags: tags - }, util: { - debounce: debounce, - markMatch: markMatch - }, "class": { - "abstract": AbstractSelect2, - "single": SingleSelect2, - "multi": MultiSelect2 - } - }; - -}(jQuery)); |