diff --git a/.eslintrc b/.eslintrc index cff9bc9a3..fb65da8f3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,7 +12,8 @@ "parser": "@babel/eslint-parser", "plugins": [ "react", - "react-hooks" + "react-hooks", + "simple-import-sort" ], "parserOptions": { "ecmaVersion": 6, @@ -56,7 +57,7 @@ "no-unsafe-negation": 2, "no-nested-ternary": 2, "no-unused-vars": [ - 2, + "error", { "vars": "all", "args": "none", @@ -73,12 +74,26 @@ "react/prop-types": 1, "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": [ - "warn", + "error", { "additionalHooks": "useCachedCallback" } ], - "use-isnan": 2 + "use-isnan": 2, + "simple-import-sort/imports": [ + "error", + { + "groups": [ + ["^react$", "^classnames$", "^[a-z]"], + ["^@(common|public|uikit)"], + ["^@"], + ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], + ["^.+(\\.mod)?\\.css$"], + ["^.+\\.(png|gif|jpe?g|webm|mov|svg)$"], + ["^\\u0000"] + ] + } + ] }, "overrides": [ { diff --git a/.gitignore b/.gitignore index cc21c8fc7..f11515aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ build node_modules/ dist/ tracker/static/gen/ +geckodriver.log yarn-error.log -TEST*.xml +test-results diff --git a/README.md b/README.md index eee9248f0..2047990db 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,15 @@ ## Requirements -- Python 3.6, 3.7 (3.8 is untested) +- Python 3.7 to 3.10 +- Django 2.2, 3.2 + +Django 3.0, 3.1, and 4.0 are not officially supported but may work with some tweaking. Additionally, if you are planning on developing, and/or building the JS bundles yourself: -- Node 12.x +- Node 14, or 16 (17+ is known to not work currently, see + [this issue in webpack](https://github.com/webpack/webpack/issues/14532)) - `yarn` (`npm i -g yarn`) - `pre-commit` (`pip install pre-commit`) @@ -16,8 +20,13 @@ very helpful. ## Deploying This app shouldn't require any special treatment to deploy, though depending on which feature set you are using, extra -steps will be required. You should be able to install it with pip, either from PyPI (preferred so that you don't have -to build the JS bundles yourself), GitHub, or locally. +steps will be required. You should be able to install it with pip, either from GitHub, or locally. e.g. + +`pip install git+https://github.com/GamesDoneQuick/donation-tracker.git@master` + +Or after downloading or checking out locally: + +`pip install ./donation-tracker` For further reading on what else your server needs to look like: @@ -131,6 +140,14 @@ Add the following parameter in `setting.py`: DOMAIN = "server hostname" ``` +To enable analytics tracking, add the following to the `MIDDLEWARE` section of `tracker_development/settings.py`: + +``` + 'tracker.analytics.middleware.AnalyticsMiddleware', +``` + +NOTE: The analytics middleware is only a client, and does not track any information locally. Instead, it expects an analytics server to be running and will simply send out HTTP requests to it when enabled. More information is available in `tracker/analytics/README.md`. + Add the following chunk somewhere in `settings.py`: ```python @@ -138,6 +155,12 @@ from tracker import ajax_lookup_channels AJAX_LOOKUP_CHANNELS = ajax_lookup_channels.AJAX_LOOKUP_CHANNELS ASGI_APPLICATION = 'tracker_development.routing.application' CHANNEL_LAYERS = {'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}} + +# Only required if analytics tracking is enabled +TRACKER_ANALYTICS_INGEST_HOST = 'http://localhost:5000' +TRACKER_ANALYTICS_NO_EMIT = False +TRACKER_ANALYTICS_TEST_MODE = False +TRACKER_ANALYTICS_ACCESS_KEY = 'someanalyticsaccesskey or None' ``` Create a file next called `routing.py` next to `settings.py` and put the following in it: @@ -171,8 +194,8 @@ import tracker.urls import ajax_select.urls urlpatterns = [ - path('admin/', admin.site.urls), path('admin/lookups/', include(ajax_select.urls)), + path('admin/', admin.site.urls), path('tracker/', include(tracker.urls, namespace='tracker')), ] ``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 28e1671b7..7b163bab5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,10 +10,24 @@ jobs: displayName: Tracker Backend strategy: matrix: - Python36: - PYTHON_VERSION: '3.6' + Latest: + PYTHON_VERSION: '3.10' + DJANGO_VERSION: '3.2' + Oldest: + PYTHON_VERSION: '3.7' + DJANGO_VERSION: '2.2' + Django22: + PYTHON_VERSION: '3.9' + DJANGO_VERSION: '2.2' Python37: PYTHON_VERSION: '3.7' + DJANGO_VERSION: '3.2' + Python38: + PYTHON_VERSION: '3.8' + DJANGO_VERSION: '3.2' + Python39: + PYTHON_VERSION: '3.9' + DJANGO_VERSION: '3.2' steps: - task: UsePythonVersion@0 @@ -23,7 +37,7 @@ jobs: - task: CacheBeta@1 inputs: - key: pip | $(Agent.OS) | tests/requirements.txt | setup.py + key: pip | $(Agent.OS) | "$(DJANGO_VERSION)" | tests/requirements.txt | setup.py path: $(Pipeline.Workspace)/../../.cache/pip displayName: 'Cache pip' @@ -35,9 +49,11 @@ jobs: - script: | python -m pip install --upgrade pip setuptools wheel - pip install -e . - pip install -r tests/requirements.txt - displayName: 'Install python prerequisites' + displayName: 'Install Python base packages' + + - script: | + pip install . -r tests/requirements.txt django~=$(DJANGO_VERSION) + displayName: 'Install Python prerequisites' - script: | python check_migrations.py @@ -79,11 +95,21 @@ jobs: continueOnError: true variables: YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn + strategy: + matrix: + Node14: + NODE_VERSION: 14 + Node16: + NODE_VERSION: 16 steps: + - task: NodeTool@0 + inputs: + versionSpec: '$(NODE_VERSION)' + - task: Cache@2 inputs: - key: yarn | $(Agent.OS) | development | yarn.lock + key: yarn | $(Agent.OS) | "$(NODE_VERSION)" | development | yarn.lock path: $(YARN_CACHE_FOLDER) displayName: 'Cache yarn' diff --git a/bundles/admin/app.js b/bundles/admin/app.js index dd83e1f41..e95fc0154 100644 --- a/bundles/admin/app.js +++ b/bundles/admin/app.js @@ -1,17 +1,19 @@ import React from 'react'; +import Loadable from 'react-loadable'; +import { useDispatch, useSelector } from 'react-redux'; import { Route, Switch, useRouteMatch } from 'react-router'; import { Link } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; -import Loadable from 'react-loadable'; -import Spinner from '../public/spinner'; -import Dropdown from '../public/dropdown'; -import { actions } from '../public/api'; +import { useConstants } from '@common/Constants'; +import Loading from '@common/Loading'; +import { actions } from '@public/api'; +import { usePermission } from '@public/api/helpers/auth'; +import Dropdown from '@public/dropdown'; +import Spinner from '@public/spinner'; + +import { setAPIRoot } from '@tracker/Endpoints'; + import ScheduleEditor from './scheduleEditor'; -import Loading from '../common/Loading'; -import { useConstants } from '../common/Constants'; -import { setAPIRoot } from '../tracker/Endpoints'; -import { usePermission } from '../public/api/helpers/auth'; const Interstitials = Loadable({ loader: () => import('./interstitials' /* webpackChunkName: 'interstitials' */), diff --git a/bundles/admin/donationProcessing/donations.mod.css b/bundles/admin/donationProcessing/donations.mod.css index a316d9ba7..80cf5cb02 100644 --- a/bundles/admin/donationProcessing/donations.mod.css +++ b/bundles/admin/donationProcessing/donations.mod.css @@ -1,6 +1,7 @@ td.comment { max-width: 60vw; overflow-wrap: break-word; + white-space: pre-wrap; } td.status { diff --git a/bundles/admin/donationProcessing/processDonations.tsx b/bundles/admin/donationProcessing/processDonations.tsx index f5c40489d..6166b66c5 100644 --- a/bundles/admin/donationProcessing/processDonations.tsx +++ b/bundles/admin/donationProcessing/processDonations.tsx @@ -1,14 +1,16 @@ import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router'; -import modelActions from '../../public/api/actions/models'; -import { usePermission } from '../../public/api/helpers/auth'; -import { useConstants } from '../../common/Constants'; -import Spinner from '../../public/spinner'; + +import { useConstants } from '@common/Constants'; +import ModelErrors from '@common/ModelErrors'; +import modelActions from '@public/api/actions/models'; +import { usePermission } from '@public/api/helpers/auth'; +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import { useFetchDonors } from '@public/hooks/useFetchDonors'; +import Spinner from '@public/spinner'; import styles from './donations.mod.css'; -import { useCachedCallback } from '../../public/hooks/useCachedCallback'; -import { useFetchDonors } from '../../public/hooks/useFetchDonors'; type Mode = 'confirm' | 'regular'; type Action = 'approved' | 'sent' | 'blocked'; @@ -29,7 +31,7 @@ const stateMap = { export default React.memo(function ProcessDonations() { const { ADMIN_ROOT } = useConstants(); - const { event: eventId } = useParams(); + const { event: eventId } = useParams<{ event: string }>(); const status = useSelector((state: any) => state.status); const donations = useSelector((state: any) => state.models.donation); const donors = useSelector((state: any) => state.models.donor); @@ -37,7 +39,7 @@ export default React.memo(function ProcessDonations() { const dispatch = useDispatch(); const canApprove = usePermission('tracker.send_to_reader'); const canEditDonors = usePermission('tracker.change_donor'); - const [partitionId, setPartitionId] = useState(0); + const [partitionId, setPartitionId] = useState(1); const [partitionCount, setPartitionCount] = useState(1); const [mode, setMode] = useState('regular'); const setProcessingMode = useCallback((e: React.ChangeEvent) => { @@ -52,7 +54,7 @@ export default React.memo(function ProcessDonations() { all_comments: '', event: eventId, }; - if (secondStep) { + if (mode === 'confirm') { params.readstate = 'FLAGGED'; } else { params.feed = 'toprocess'; @@ -60,7 +62,7 @@ export default React.memo(function ProcessDonations() { dispatch(modelActions.loadModels('donation', params)); e?.preventDefault(); }, - [dispatch, secondStep, eventId], + [dispatch, eventId, mode], ); useFetchDonors(eventId); useEffect(() => { @@ -92,6 +94,13 @@ export default React.memo(function ProcessDonations() { }, [dispatch], ); + const sortedDonations = useMemo(() => { + return donations + ? donations + .filter((donation: any) => donation.pk % partitionCount === partitionId - 1) + .sort((a: any, b: any) => b.pk - a.pk) + : []; + }, [donations, partitionCount, partitionId]); return (
@@ -100,87 +109,95 @@ export default React.memo(function ProcessDonations() { {canApprove && !event?.use_one_step_screening && ( )} - +
+ - {donations - ?.filter((donation: any) => donation.pk % partitionCount === partitionId) - .map((donation: any) => { - const donor = donors?.find((d: any) => d.pk === donation.donor); - const donorLabel = donor?.alias ? `${donor.alias}#${donor.alias_num}` : '(Anonymous)'; + {sortedDonations.map((donation: any) => { + const donor = donors?.find((d: any) => d.pk === donation.donor); + const donorLabel = donor?.alias ? `${donor.alias}#${donor.alias_num}` : '(Anonymous)'; - return ( - - - - - - - - ); - })} + return ( + + + + + + + + ); + })}
- {canEditDonors ? {donorLabel} : donorLabel} - - ¥{(+donation.amount).toFixed(0)} - {donation.comment} - - - - - - {donationState[donation.pk] && stateMap[donationState[donation.pk]]} - -
+ {canEditDonors ? {donorLabel} : donorLabel} + + ¥{(+donation.amount).toFixed(0)} + {donation.comment} + + + + + + {donationState[donation.pk] && stateMap[donationState[donation.pk]]} + +
diff --git a/bundles/admin/donationProcessing/processDonationsSpec.tsx b/bundles/admin/donationProcessing/processDonationsSpec.tsx index d59126653..2b16aae6a 100644 --- a/bundles/admin/donationProcessing/processDonationsSpec.tsx +++ b/bundles/admin/donationProcessing/processDonationsSpec.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import thunk from 'redux-thunk'; -import configureMockStore from 'redux-mock-store'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; import { Provider } from 'react-redux'; import { Route, StaticRouter } from 'react-router'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; import ProcessDonations from './processDonations'; -import Endpoints from '../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); @@ -17,11 +18,17 @@ describe('ProcessDonations', () => { const eventId = 1; beforeEach(() => { + jasmine.clock().install(); fetchMock.restore(); }); + afterEach(() => { + jasmine.clock().uninstall(); + }); + it('loads donors and donations on mount', () => { render({}); + jasmine.clock().tick(0); expect(store.getActions()).toContain(jasmine.objectContaining({ type: 'MODEL_STATUS_LOADING', model: 'donor' })); expect(store.getActions()).toContain(jasmine.objectContaining({ type: 'MODEL_STATUS_LOADING', model: 'donation' })); }); diff --git a/bundles/admin/donationProcessing/processPendingBids.tsx b/bundles/admin/donationProcessing/processPendingBids.tsx index 63366f09d..133a9b84c 100644 --- a/bundles/admin/donationProcessing/processPendingBids.tsx +++ b/bundles/admin/donationProcessing/processPendingBids.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useEffect, useReducer } from 'react'; -import { useConstants } from '../../common/Constants'; -import { useParams } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; -import modelActions from '../../public/api/actions/models'; -import { useCachedCallback } from '../../public/hooks/useCachedCallback'; -import Spinner from '../../public/spinner'; +import { useParams } from 'react-router'; + +import { useConstants } from '@common/Constants'; +import modelActions from '@public/api/actions/models'; +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import { useFetchParents } from '@public/hooks/useFetchParents'; +import Spinner from '@public/spinner'; + import styles from './donations.mod.css'; -import { useFetchParents } from '../../public/hooks/useFetchParents'; type Action = 'accept' | 'deny'; @@ -25,7 +27,7 @@ const stateMap = { export default React.memo(function ProcessPendingBids() { const { ADMIN_ROOT } = useConstants(); - const { event: eventId } = useParams(); + const { event: eventId } = useParams<{ event: string }>(); const status = useSelector((state: any) => state.status); const bids = useSelector((state: any) => state.models.bid); const event = useSelector((state: any) => state.models.event?.find((e: any) => e.pk === +eventId!)); diff --git a/bundles/admin/donationProcessing/processPendingBidsSpec.tsx b/bundles/admin/donationProcessing/processPendingBidsSpec.tsx index 704909674..a9529f2be 100644 --- a/bundles/admin/donationProcessing/processPendingBidsSpec.tsx +++ b/bundles/admin/donationProcessing/processPendingBidsSpec.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import thunk from 'redux-thunk'; -import configureMockStore from 'redux-mock-store'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; import { Provider } from 'react-redux'; import { Route, StaticRouter } from 'react-router'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; import ProcessPendingBids from './processPendingBids'; -import Endpoints from '../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); diff --git a/bundles/admin/donationProcessing/readDonations.tsx b/bundles/admin/donationProcessing/readDonations.tsx index 7b4853b02..121c99c65 100644 --- a/bundles/admin/donationProcessing/readDonations.tsx +++ b/bundles/admin/donationProcessing/readDonations.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; -import { useConstants } from '../../common/Constants'; -import { useParams } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; -import { usePermission } from '../../public/api/helpers/auth'; -import modelActions from '../../public/api/actions/models'; -import { useCachedCallback } from '../../public/hooks/useCachedCallback'; -import Spinner from '../../public/spinner'; -import { useFetchDonors } from '../../public/hooks/useFetchDonors'; +import { useParams } from 'react-router'; + +import { useConstants } from '@common/Constants'; +import modelActions from '@public/api/actions/models'; +import { usePermission } from '@public/api/helpers/auth'; +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import { useFetchDonors } from '@public/hooks/useFetchDonors'; +import Spinner from '@public/spinner'; + import styles from './donations.mod.css'; type Action = 'read' | 'ignored' | 'blocked'; @@ -27,7 +29,7 @@ const stateMap = { export default React.memo(function ReadDonations() { const { ADMIN_ROOT } = useConstants(); - const { event: eventId } = useParams(); + const { event: eventId } = useParams<{ event: string }>(); const status = useSelector((state: any) => state.status); const donations = useSelector((state: any) => state.models.donation); const donors = useSelector((state: any) => state.models.donor); diff --git a/bundles/admin/donationProcessing/readDonationsSpec.tsx b/bundles/admin/donationProcessing/readDonationsSpec.tsx index bcf18cfe0..f90bc9374 100644 --- a/bundles/admin/donationProcessing/readDonationsSpec.tsx +++ b/bundles/admin/donationProcessing/readDonationsSpec.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import thunk from 'redux-thunk'; -import configureMockStore from 'redux-mock-store'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; import { Provider } from 'react-redux'; import { Route, StaticRouter } from 'react-router'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; import ReadDonations from './readDonations'; -import Endpoints from '../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); @@ -17,11 +18,17 @@ describe('ReadDonations', () => { const eventId = 1; beforeEach(() => { + jasmine.clock().install(); fetchMock.restore(); }); + afterEach(() => { + jasmine.clock().uninstall(); + }); + it('loads donors and donations on mount', () => { render({}); + jasmine.clock().tick(0); expect(store.getActions()).toContain(jasmine.objectContaining({ type: 'MODEL_STATUS_LOADING', model: 'donor' })); expect(store.getActions()).toContain(jasmine.objectContaining({ type: 'MODEL_STATUS_LOADING', model: 'donation' })); }); diff --git a/bundles/admin/index.js b/bundles/admin/index.js index 4d6fcb9d7..2d2d7a3d1 100644 --- a/bundles/admin/index.js +++ b/bundles/admin/index.js @@ -1,17 +1,18 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import HTML5Backend from 'react-dnd-html5-backend'; +import { ConnectedRouter } from 'connected-react-router'; import { DndProvider } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { ConnectedRouter } from 'connected-react-router'; import { Redirect, Route, Switch } from 'react-router'; -import ErrorBoundary from 'ui/public/errorBoundary'; +import Constants from '@common/Constants'; +import { createTrackerStore } from '@public/api'; +import ErrorBoundary from '@public/errorBoundary'; -import '../common/init'; import App from './app'; -import Constants from '../common/Constants'; -import { createTrackerStore } from '../public/api'; + +import '@common/init'; window.AdminApp = function (props) { function redirect({ location }) { diff --git a/bundles/admin/interstitials/Editor/Body.tsx b/bundles/admin/interstitials/Editor/Body.tsx index c5fce8d93..1556c22d9 100644 --- a/bundles/admin/interstitials/Editor/Body.tsx +++ b/bundles/admin/interstitials/Editor/Body.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; -import { Ad, Interview, Model, ModelFields, Run } from '../../../common/Models'; -import { fullKey, memoizeCallback } from '../../../common/Util'; + +import { Ad, Interview, Model, ModelFields, Run } from '@common/Models'; +import { fullKey, memoizeCallback } from '@common/Util'; + import Row from './Rows'; const cachedCallback = memoizeCallback( diff --git a/bundles/admin/interstitials/Editor/Rows.tsx b/bundles/admin/interstitials/Editor/Rows.tsx index 5468f137b..730ee21b1 100644 --- a/bundles/admin/interstitials/Editor/Rows.tsx +++ b/bundles/admin/interstitials/Editor/Rows.tsx @@ -1,11 +1,13 @@ import React from 'react'; import cn from 'classnames'; -import { Ad, Interview, ModelFields, Run } from '../../../common/Models'; -import styles from './index.mod.css'; +import { Ad, Interview, ModelFields, Run } from '@common/Models'; + import AdRow, { AdRowProps } from './Rows/AdRow'; -import SpeedrunRow, { SpeedrunRowProps } from './Rows/SpeedrunRow'; import InterviewRow, { InterviewRowProps } from './Rows/InterviewRow'; +import SpeedrunRow, { SpeedrunRowProps } from './Rows/SpeedrunRow'; + +import styles from './index.mod.css'; export interface RowProps { item: Interview | Ad | Run; diff --git a/bundles/admin/interstitials/Editor/Rows/AdRow.tsx b/bundles/admin/interstitials/Editor/Rows/AdRow.tsx index d60ba9a4a..b1d34a9a7 100644 --- a/bundles/admin/interstitials/Editor/Rows/AdRow.tsx +++ b/bundles/admin/interstitials/Editor/Rows/AdRow.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import { RowProps } from '../Rows.js'; -import { Ad } from '../../../../common/Models'; + +import { Ad } from '@common/Models'; + +import { RowProps } from '../Rows'; export interface AdRowProps extends RowProps { item: Ad; diff --git a/bundles/admin/interstitials/Editor/Rows/InterviewRow.tsx b/bundles/admin/interstitials/Editor/Rows/InterviewRow.tsx index 14ad6af32..8e6324415 100644 --- a/bundles/admin/interstitials/Editor/Rows/InterviewRow.tsx +++ b/bundles/admin/interstitials/Editor/Rows/InterviewRow.tsx @@ -1,6 +1,8 @@ -import { fullKey, memoizeCallback } from '../../../../common/Util'; import React, { useState } from 'react'; -import { Interview, InterviewFields } from '../../../../common/Models'; + +import { Interview, InterviewFields } from '@common/Models'; +import { fullKey, memoizeCallback } from '@common/Util'; + import { RowProps } from '../Rows'; const setStateCallback = memoizeCallback( diff --git a/bundles/admin/interstitials/Editor/Rows/SpeedrunRow.tsx b/bundles/admin/interstitials/Editor/Rows/SpeedrunRow.tsx index fca32b3b4..837d6d4ca 100644 --- a/bundles/admin/interstitials/Editor/Rows/SpeedrunRow.tsx +++ b/bundles/admin/interstitials/Editor/Rows/SpeedrunRow.tsx @@ -1,7 +1,9 @@ import React from 'react'; import moment from 'moment-timezone'; -import { RowProps } from '../Rows.js'; -import { Run } from '../../../../common/Models'; + +import { Run } from '@common/Models'; + +import { RowProps } from '../Rows'; export interface SpeedrunRowProps extends RowProps { item: Run; diff --git a/bundles/admin/interstitials/Editor/Table.tsx b/bundles/admin/interstitials/Editor/Table.tsx index 0f6233145..2b16cf104 100644 --- a/bundles/admin/interstitials/Editor/Table.tsx +++ b/bundles/admin/interstitials/Editor/Table.tsx @@ -1,10 +1,13 @@ -import styles from './index.mod.css'; import React from 'react'; -import { Ad, Interstitial, Interview, Model, ModelFields, Run } from '../../../common/Models'; -import { ServerError } from '../../../common/Server'; -import TableRowErrorDisplay from '../../../common/TableRowErrorDisplay'; + +import { Ad, Interstitial, Interview, Model, ModelFields, Run } from '@common/Models'; +import { sortItems } from '@common/Ordered'; +import { ServerError } from '@common/Server'; +import TableRowErrorDisplay from '@common/TableRowErrorDisplay'; + import Body from './Body'; -import { sortItems } from '../../../common/Ordered'; + +import styles from './index.mod.css'; interface Props { saveItem: (key: string, fields: Partial) => void; diff --git a/bundles/admin/interstitials/Editor/index.tsx b/bundles/admin/interstitials/Editor/index.tsx index 12ae2df67..f46a1e7b1 100644 --- a/bundles/admin/interstitials/Editor/index.tsx +++ b/bundles/admin/interstitials/Editor/index.tsx @@ -1,13 +1,14 @@ import React, { useCallback, useEffect, useState } from 'react'; -import Table from './Table'; -import { JSONResponse, JSONResponseWithForbidden } from '../../../common/JSONResponse'; -import { Ad, Interstitial, Interview, ModelFields, Run } from '../../../common/Models'; -import { isServerError, ServerError } from '../../../common/Server'; - -import usePromise from 'react-use-promise'; import moment from 'moment-timezone'; import { useParams } from 'react-router'; -import { useConstants } from '../../../common/Constants'; +import usePromise from 'react-use-promise'; + +import { useConstants } from '@common/Constants'; +import { JSONResponse, JSONResponseWithForbidden } from '@common/JSONResponse'; +import { Ad, Interstitial, Interview, ModelFields, Run } from '@common/Models'; +import { isServerError, ServerError } from '@common/Server'; + +import Table from './Table'; interface Person { permissions?: string[]; @@ -19,7 +20,7 @@ type Aggregate = [Interview[], Ad[] | ServerError, Run[], Person | ServerError]; export default function InterstitialEditor() { const { API_ROOT, CSRF_TOKEN } = useConstants(); - const { event } = useParams(); + const { event } = useParams<{ event: string }>(); const [promise, setPromise] = useState>(new Promise(() => {})); const [saveError, setSaveError] = useState(null); const fetchAll = useCallback(() => { diff --git a/bundles/admin/scheduleEditor/dragDrop/speedrunDropTarget.js b/bundles/admin/scheduleEditor/dragDrop/speedrunDropTarget.js index e95a97f45..c6f66e314 100644 --- a/bundles/admin/scheduleEditor/dragDrop/speedrunDropTarget.js +++ b/bundles/admin/scheduleEditor/dragDrop/speedrunDropTarget.js @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { DropTarget } from 'react-dnd'; -import Constants from '../../../common/Constants'; + +import Constants from '@common/Constants'; class SpeedrunDropTarget extends React.Component { static propTypes = { diff --git a/bundles/admin/scheduleEditor/index.js b/bundles/admin/scheduleEditor/index.js index 81389b8a0..a32bda766 100644 --- a/bundles/admin/scheduleEditor/index.js +++ b/bundles/admin/scheduleEditor/index.js @@ -1,9 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; -import { actions } from 'ui/public/api'; -import Spinner from 'ui/public/spinner'; -import authHelper from 'ui/public/api/helpers/auth'; +import { actions } from '@public/api'; +import authHelper from '@public/api/helpers/auth'; +import Spinner from '@public/spinner'; import SpeedrunTable from './speedrunTable'; diff --git a/bundles/admin/scheduleEditor/indexSpec.tsx b/bundles/admin/scheduleEditor/indexSpec.tsx index d8ff0bf6f..abef34a45 100644 --- a/bundles/admin/scheduleEditor/indexSpec.tsx +++ b/bundles/admin/scheduleEditor/indexSpec.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; -import ScheduleEditor from './index'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; import { mount } from 'enzyme'; -import { Provider } from 'react-redux'; import fetchMock from 'fetch-mock'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import ScheduleEditor from './index'; const mockStore = configureMockStore([thunk]); diff --git a/bundles/admin/scheduleEditor/speedrun.js b/bundles/admin/scheduleEditor/speedrun.js index 98d99a8d7..39ce5b21a 100644 --- a/bundles/admin/scheduleEditor/speedrun.js +++ b/bundles/admin/scheduleEditor/speedrun.js @@ -1,13 +1,13 @@ -import _ from 'lodash'; import React from 'react'; +import _ from 'lodash'; +import moment from 'moment'; import PropTypes from 'prop-types'; import { DragSource } from 'react-dnd'; -import moment from 'moment'; -import Spinner from '../../public/spinner'; -import OrderTarget from '../../public/orderTarget'; -import FormField from '../../public/formField'; -import ErrorList from '../../public/errorList'; +import ErrorList from '@public/errorList'; +import FormField from '@public/formField'; +import OrderTarget from '@public/orderTarget'; +import Spinner from '@public/spinner'; import SpeedrunDropTarget from './dragDrop/speedrunDropTarget'; diff --git a/bundles/admin/scheduleEditor/speedrunTable.js b/bundles/admin/scheduleEditor/speedrunTable.js index a01522248..b17ae6a24 100644 --- a/bundles/admin/scheduleEditor/speedrunTable.js +++ b/bundles/admin/scheduleEditor/speedrunTable.js @@ -1,9 +1,10 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import Speedrun from './speedrun.js'; +import ErrorList from '@public/errorList'; + import EmptyTableDropTarget from './dragDrop/emptyTableDropTarget'; -import ErrorList from 'ui/public/errorList'; +import Speedrun from './speedrun.js'; function orderSort(a, b) { if (a.order === null && b.order === null) { @@ -66,7 +67,7 @@ function SpeedrunTable({ 'order', 1, ), - [], + [saveField, speedruns], ); // this is hard as hell to understand and kinda slow so uh maybe clean it up a bit diff --git a/bundles/admin/scheduleImport/scheduleImport.tsx b/bundles/admin/scheduleImport/scheduleImport.tsx index 0bb8800a3..8ef1d59d9 100644 --- a/bundles/admin/scheduleImport/scheduleImport.tsx +++ b/bundles/admin/scheduleImport/scheduleImport.tsx @@ -1,11 +1,13 @@ -import { getSchedule, OengusSchedule } from 'oengus-api'; import React, { useState } from 'react'; +import { getSchedule, OengusSchedule } from 'oengus-api'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router'; -import styles from './scheduleImport.mod.css'; + import { ScheduleImportCore } from './scheduleImportCore'; import { ScheduleImportTable } from './scheduleImportTable'; +import styles from './scheduleImport.mod.css'; + export type EventModel = { pk: number; name: string; diff --git a/bundles/admin/scheduleImport/scheduleImportCore.tsx b/bundles/admin/scheduleImport/scheduleImportCore.tsx index c1cb4f7b6..882efb6b2 100644 --- a/bundles/admin/scheduleImport/scheduleImportCore.tsx +++ b/bundles/admin/scheduleImport/scheduleImportCore.tsx @@ -1,12 +1,13 @@ +import React, { useState } from 'react'; +import _ from 'lodash'; import moment from 'moment'; import { OengusRunLine, OengusSchedule, OengusSetupLine, OengusUser } from 'oengus-api'; -import React, { useState } from 'react'; +import { OengusOtherLine } from 'oengus-api/dist/types'; + import { Model, Run, Runner } from '../../common/Models'; import HTTPUtil from '../../public/util/http'; import Endpoints from '../../tracker/Endpoints'; -import _ from 'lodash'; import { EventModel } from './scheduleImport'; -import { OengusOtherLine } from 'oengus-api/dist/types'; type Props = { event: EventModel; diff --git a/bundles/admin/scheduleImport/scheduleImportTable.tsx b/bundles/admin/scheduleImport/scheduleImportTable.tsx index f7334b0e1..5245dd82b 100644 --- a/bundles/admin/scheduleImport/scheduleImportTable.tsx +++ b/bundles/admin/scheduleImport/scheduleImportTable.tsx @@ -1,6 +1,7 @@ +import React, { ReactFragment } from 'react'; import moment from 'moment'; import { OengusConnection, OengusRunLine, OengusSchedule } from 'oengus-api'; -import React, { ReactFragment } from 'react'; + import styles from './scheduleImportTable.mod.css'; type Props = { diff --git a/bundles/common/Constants.tsx b/bundles/common/Constants.tsx index 41c384bd5..c6ddff495 100644 --- a/bundles/common/Constants.tsx +++ b/bundles/common/Constants.tsx @@ -3,6 +3,7 @@ import React, { useContext } from 'react'; export const DefaultConstants = { PRIVACY_POLICY_URL: '', SWEEPSTAKES_URL: '', + ANALYTICS_URL: '', API_ROOT: '', ADMIN_ROOT: '', STATIC_URL: '/static/', diff --git a/bundles/common/ModelErrors.tsx b/bundles/common/ModelErrors.tsx new file mode 100644 index 000000000..8ca76c17b --- /dev/null +++ b/bundles/common/ModelErrors.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +export default React.memo(function ModelErrors() { + const status = useSelector((state: any) => state.status); + return ( + <> + {Object.entries(status).map(([model, status]: [string, any]) => status === 'error' &&
{model}: Error
)} + + ); +}); diff --git a/bundles/common/Ordered.ts b/bundles/common/Ordered.ts index 23bdef571..d26b1e8d0 100644 --- a/bundles/common/Ordered.ts +++ b/bundles/common/Ordered.ts @@ -1,5 +1,6 @@ -import { Ordered, Subordered } from './Models'; import memoize from 'memoize-one'; + +import { Ordered, Subordered } from './Models'; import { fullKey, isSubordered } from './Util'; export const sortItems = memoize((models: Ordered[]) => { diff --git a/bundles/common/TableRowErrorDisplay.tsx b/bundles/common/TableRowErrorDisplay.tsx index 2114ab9f3..2028a9d9f 100644 --- a/bundles/common/TableRowErrorDisplay.tsx +++ b/bundles/common/TableRowErrorDisplay.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { isValidationError, ServerError, ValidationError } from './Server'; interface ErrorDisplayProps { diff --git a/bundles/public/api/actions/index.js b/bundles/public/api/actions/index.js index e432836eb..df37689b2 100644 --- a/bundles/public/api/actions/index.js +++ b/bundles/public/api/actions/index.js @@ -1,5 +1,5 @@ -import models from './models'; import dropdowns from './dropdowns'; +import models from './models'; import singletons from './singletons'; export default { diff --git a/bundles/public/api/actions/models.js b/bundles/public/api/actions/models.js index 72084f426..bac468bab 100644 --- a/bundles/public/api/actions/models.js +++ b/bundles/public/api/actions/models.js @@ -1,7 +1,8 @@ import _ from 'lodash'; +import Endpoints from '@tracker/Endpoints'; + import HTTPUtil from '../../util/http'; -import Endpoints from '../../../tracker/Endpoints'; function onModelStatusLoad(model) { return { @@ -70,7 +71,6 @@ function loadModels(model, params, additive) { type: fetchModel, }) .then(models => { - dispatch(onModelStatusSuccess(model)); const action = additive ? onModelCollectionAdd : onModelCollectionReplace; dispatch( action( @@ -84,12 +84,13 @@ function loadModels(model, params, additive) { }, []), ), ); + dispatch(onModelStatusSuccess(model)); }) .catch(error => { - dispatch(onModelStatusError(model)); if (!additive) { dispatch(onModelCollectionReplace(realModel, [])); } + dispatch(onModelStatusError(model)); }); }; } diff --git a/bundles/public/api/actions/singletons.js b/bundles/public/api/actions/singletons.js index 0820be6b1..e3ac0c9dd 100644 --- a/bundles/public/api/actions/singletons.js +++ b/bundles/public/api/actions/singletons.js @@ -1,5 +1,6 @@ +import Endpoints from '@tracker/Endpoints'; + import HTTPUtil from '../../util/http'; -import Endpoints from '../../../tracker/Endpoints'; function onLoadMe(me) { return { diff --git a/bundles/public/api/actions/singletonsSpec.js b/bundles/public/api/actions/singletonsSpec.js index f98915a0b..80476f781 100644 --- a/bundles/public/api/actions/singletonsSpec.js +++ b/bundles/public/api/actions/singletonsSpec.js @@ -1,9 +1,10 @@ import fetchMock from 'fetch-mock'; -import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; import singletons from './singletons'; -import Endpoints from '../../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); diff --git a/bundles/public/api/index.js b/bundles/public/api/index.js index 2550cb68a..ec79f234f 100644 --- a/bundles/public/api/index.js +++ b/bundles/public/api/index.js @@ -1,11 +1,11 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; import { routerMiddleware } from 'connected-react-router'; +import { createBrowserHistory } from 'history'; +import { applyMiddleware, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; +import thunk from 'redux-thunk'; -import { createBrowserHistory } from 'history'; +import freeze from '@public/util/freeze'; -import freeze from 'ui/public/util/freeze'; import actions from './actions'; import createRootReducer from './reducers'; diff --git a/bundles/public/api/reducers/index.js b/bundles/public/api/reducers/index.js index a6942a693..dd971d17e 100644 --- a/bundles/public/api/reducers/index.js +++ b/bundles/public/api/reducers/index.js @@ -1,11 +1,11 @@ -import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; +import { combineReducers } from 'redux'; import drafts from './drafts'; -import models from './models'; -import status from './status'; import dropdowns from './dropdowns'; +import models from './models'; import singletons from './singletons'; +import status from './status'; const createRootReducer = history => combineReducers({ diff --git a/bundles/public/dropdown.tsx b/bundles/public/dropdown.tsx index d6ed93465..48c1a177d 100644 --- a/bundles/public/dropdown.tsx +++ b/bundles/public/dropdown.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import invariant from 'invariant'; -import { useConstants } from '../common/Constants'; + +import { useConstants } from '@common/Constants'; interface DropdownProps { closeOnClick?: boolean; diff --git a/bundles/public/dropdownSpec.tsx b/bundles/public/dropdownSpec.tsx index d0738ed9e..848c9ca6d 100644 --- a/bundles/public/dropdownSpec.tsx +++ b/bundles/public/dropdownSpec.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import { shallow } from 'enzyme'; import Dropdown from './dropdown'; -import { shallow } from 'enzyme'; describe('Dropdown', () => { let subject: ReturnType; diff --git a/bundles/public/errorBoundary.tsx b/bundles/public/errorBoundary.tsx index a7be0145c..d90a0003f 100644 --- a/bundles/public/errorBoundary.tsx +++ b/bundles/public/errorBoundary.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import Header from '../uikit/Header'; -import Text from '../uikit/Text'; +import Header from '@uikit/Header'; +import Text from '@uikit/Text'; interface ErrorBoundaryInternalProps { onError: (error: Error) => void; diff --git a/bundles/public/errorListSpec.tsx b/bundles/public/errorListSpec.tsx index 3cf866e09..539e1b9f0 100644 --- a/bundles/public/errorListSpec.tsx +++ b/bundles/public/errorListSpec.tsx @@ -1,6 +1,7 @@ -import ErrorList from './errorList'; -import { shallow } from 'enzyme'; import React from 'react'; +import { shallow } from 'enzyme'; + +import ErrorList from './errorList'; describe('ErrorList', () => { it('displays nothing if given a nullish value', () => { diff --git a/bundles/public/hooks/useCachedCallback.ts b/bundles/public/hooks/useCachedCallback.ts index 5209af331..4ea5dd3a2 100644 --- a/bundles/public/hooks/useCachedCallback.ts +++ b/bundles/public/hooks/useCachedCallback.ts @@ -1,6 +1,6 @@ +import { useCallback, useMemo, useRef } from 'react'; import invariant from 'invariant'; import { isObject } from 'lodash'; -import { useCallback, useMemo, useRef } from 'react'; function JSONKey(...args: any[]): string { return args.length > 1 || isObject(args[0]) || args[0] == null ? JSON.stringify(args) : args[0].toString(); diff --git a/bundles/public/hooks/useFetchDonors.ts b/bundles/public/hooks/useFetchDonors.ts index 544c54d63..83005f20d 100644 --- a/bundles/public/hooks/useFetchDonors.ts +++ b/bundles/public/hooks/useFetchDonors.ts @@ -1,26 +1,37 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + import modelActions from '../api/actions/models'; -import { useSelector, useDispatch } from 'react-redux'; -import { useEffect } from 'react'; export function useFetchDonors(eventId: number | string | undefined) { - const donations = useSelector((state: any) => state.models.donation); - const donors = useSelector((state: any) => state.models.donor); + const { donation: donations, donor: donors } = useSelector((state: any) => state.models); + const { donor: donorStatus } = useSelector((state: any) => state.status); const dispatch = useDispatch(); + const timeoutId = React.useRef | null>(null); - useEffect(() => { - if (!donors && eventId != null) { - dispatch(modelActions.loadModels('donor', { event: +eventId })); - } else if (donations) { - const ids = new Set( - donations - .filter( - (dn: any) => dn.donor && dn.donor__visibility !== 'ANON' && !donors?.find((dr: any) => dr.pk === dn.donor), - ) - .map((dn: any) => dn.donor), - ); - if (ids.size) { - dispatch(modelActions.loadModels('donor', { ids: [...ids.values()].join(',') }, true)); - } + React.useEffect(() => { + if (!timeoutId.current && donorStatus !== 'loading') { + timeoutId.current = setTimeout(() => { + if (!donors && eventId != null) { + dispatch(modelActions.loadModels('donor', { event: +eventId })); + } else if (donations) { + const ids = new Set( + donations + .filter( + (dn: any) => + dn.donor && + dn.donor__visibility && + dn.donor__visibility !== 'ANON' && + !donors?.find((dr: any) => dr.pk === dn.donor), + ) + .map((dn: any) => dn.donor), + ); + if (ids.size) { + dispatch(modelActions.loadModels('donor', { ids: [...ids.values()].join(',') }, true)); + } + } + timeoutId.current = null; + }, 0); } - }, [dispatch, donations, donors, eventId]); + }, [dispatch, donations, donors, donorStatus, eventId]); } diff --git a/bundles/public/hooks/useFetchDonorsSpec.tsx b/bundles/public/hooks/useFetchDonorsSpec.tsx index 018c461a8..298859d28 100644 --- a/bundles/public/hooks/useFetchDonorsSpec.tsx +++ b/bundles/public/hooks/useFetchDonorsSpec.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { Provider, useSelector } from 'react-redux'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; +import { Provider, useSelector } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; + import { useFetchDonors } from './useFetchDonors'; -import Endpoints from '../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); @@ -26,12 +28,18 @@ describe('useFetchDonors', () => { const eventId = 1; beforeEach(() => { + jasmine.clock().install(); fetchMock.restore(); }); + afterEach(() => { + jasmine.clock().uninstall(); + }); + it('fetches donors by event if donors are completely missing', () => { fetchMock.getOnce(`${Endpoints.SEARCH}?event=${eventId}&type=donor`, { body: [] }); render({}); + jasmine.clock().tick(0); expect(fetchMock.done()).toBe(true); }); @@ -57,9 +65,14 @@ describe('useFetchDonors', () => { donor: 4, donor__visibility: 'ANON', }, + { + donor: 5, + // no visibility information, e.g. the donation has been edited since the last fetch, so treat as anonymous + }, ], }, }); + jasmine.clock().tick(0); expect(fetchMock.done()).toBe(true); }); @@ -77,9 +90,14 @@ describe('useFetchDonors', () => { donor: 2, donor__visibility: 'ALIAS', }, + { + donor: 3, + // no visibility information, e.g. the donation has been edited since the last fetch, so treat as anonymous + }, ], }, }); + jasmine.clock().tick(0); expect(fetchMock.calls().length).toBe(0); }); diff --git a/bundles/public/hooks/useFetchParents.ts b/bundles/public/hooks/useFetchParents.ts index e87e9fcc0..d585b8a0d 100644 --- a/bundles/public/hooks/useFetchParents.ts +++ b/bundles/public/hooks/useFetchParents.ts @@ -1,6 +1,7 @@ -import modelActions from '../api/actions/models'; -import { useSelector, useDispatch } from 'react-redux'; import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import modelActions from '../api/actions/models'; export function useFetchParents() { const bids = useSelector((state: any) => state.models.bid); diff --git a/bundles/public/hooks/useFetchParentsSpec.tsx b/bundles/public/hooks/useFetchParentsSpec.tsx index e69d1a5a6..aa57623d8 100644 --- a/bundles/public/hooks/useFetchParentsSpec.tsx +++ b/bundles/public/hooks/useFetchParentsSpec.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { Provider, useSelector } from 'react-redux'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; +import { Provider, useSelector } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; + import { useFetchParents } from './useFetchParents'; -import Endpoints from '../../tracker/Endpoints'; const mockStore = configureMockStore([thunk]); diff --git a/bundles/public/orderTarget.tsx b/bundles/public/orderTarget.tsx index 549c803a0..c0bd709fb 100644 --- a/bundles/public/orderTarget.tsx +++ b/bundles/public/orderTarget.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { useConstants } from '@common/Constants'; + import Spinner from './spinner'; -import { useConstants } from '../common/Constants'; function OrderTarget({ connectDragSource, diff --git a/bundles/public/spinner.tsx b/bundles/public/spinner.tsx index 8af95b120..971a2a59d 100644 --- a/bundles/public/spinner.tsx +++ b/bundles/public/spinner.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { useConstants } from '../common/Constants'; + +import { useConstants } from '@common/Constants'; function Spinner({ children, @@ -12,7 +13,7 @@ function Spinner({ }) { const { STATIC_URL } = useConstants(); - return spinning ? loading : <>{children}; + return spinning ? loading : <>{children}; } export default Spinner; diff --git a/bundles/public/spinnerSpec.tsx b/bundles/public/spinnerSpec.tsx index 546546b71..1cb32a4be 100644 --- a/bundles/public/spinnerSpec.tsx +++ b/bundles/public/spinnerSpec.tsx @@ -1,7 +1,9 @@ -import Spinner from './spinner'; -import { shallow } from 'enzyme'; import React from 'react'; -import { DefaultConstants } from '../common/Constants'; +import { shallow } from 'enzyme'; + +import { DefaultConstants } from '@common/Constants'; + +import Spinner from './spinner'; describe('Spinner', () => { let subject: ReturnType; diff --git a/bundles/public/util/http.ts b/bundles/public/util/http.ts index 5cad5e01f..e7bcdb298 100644 --- a/bundles/public/util/http.ts +++ b/bundles/public/util/http.ts @@ -1,6 +1,7 @@ -import Cookies from './cookies'; import queryString from 'query-string'; +import Cookies from './cookies'; + export const Encoders = { JSON: { module: JSON, diff --git a/bundles/tracker/App.tsx b/bundles/tracker/App.tsx index 5c1791961..123eb7d31 100644 --- a/bundles/tracker/App.tsx +++ b/bundles/tracker/App.tsx @@ -1,22 +1,31 @@ import * as React from 'react'; -import { Router, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom'; +import { useConstants } from '@common/Constants'; + +import { AnalyticsEvent, setAnalyticsURL, track } from './analytics/Analytics'; import DonateInitializer from './donation/components/DonateInitializer'; import EventRouter from './events/components/EventRouter'; import NotFound from './router/components/NotFound'; -import { Routes, createTrackerHistory } from './router/RouterUtils'; -import { useConstants } from '../common/Constants'; +import { createTrackerHistory, Routes } from './router/RouterUtils'; import { setAPIRoot } from './Endpoints'; const App = (props: React.ComponentProps) => { const history = React.useMemo(() => createTrackerHistory(props.ROOT_PATH), [props.ROOT_PATH]); - const { API_ROOT } = useConstants(); + const { ANALYTICS_URL, API_ROOT } = useConstants(); const [ready, setReady] = React.useState(false); React.useEffect(() => { setAPIRoot(API_ROOT); + setAnalyticsURL(ANALYTICS_URL); setReady(true); - }, [API_ROOT]); + }, [API_ROOT, ANALYTICS_URL]); + + React.useLayoutEffect(() => { + track(AnalyticsEvent.TRACKER_APP_LOADED, { + react_render_finished_ms: Math.floor(window.performance.now()), + }); + }, []); return ( <> diff --git a/bundles/tracker/Store.ts b/bundles/tracker/Store.ts index 7ab2dfb33..0b86b3402 100644 --- a/bundles/tracker/Store.ts +++ b/bundles/tracker/Store.ts @@ -1,4 +1,4 @@ -import { createStore, applyMiddleware, combineReducers } from 'redux'; +import { applyMiddleware, combineReducers, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import thunk from 'redux-thunk'; diff --git a/bundles/tracker/analytics/Analytics.tsx b/bundles/tracker/analytics/Analytics.tsx new file mode 100644 index 000000000..0bf266061 --- /dev/null +++ b/bundles/tracker/analytics/Analytics.tsx @@ -0,0 +1,50 @@ +import * as HTTPUtils from '@public/util/http'; + +import AnalyticsEvent from './AnalyticsEvent'; + +export { AnalyticsEvent }; + +interface AnalyticsEventData { + event_name: string; + properties: Record; +} + +let ANALYTICS_URL = ''; +let EVENT_BUFFER: AnalyticsEventData[] = []; +const MAX_BUFFER_SIZE = 20; +const BUFFER_WAIT_TIME = 800; + +let flushTimeoutId: number | undefined; + +export function setAnalyticsURL(newHost: string) { + ANALYTICS_URL = newHost; +} + +export function track(event: AnalyticsEvent, properties: Record) { + // We don't want to be sending dozens of tracking requests every time + // a single thing happens. This queues events to try and group them + // into manageable batches while still guaranteeing that events are + // emitted within a reasonable amount of time. + + clearTimeout(flushTimeoutId); + EVENT_BUFFER.push({ event_name: event, properties }); + flushTimeoutId = undefined; + + if (EVENT_BUFFER.length >= MAX_BUFFER_SIZE) { + flush(); + } else { + // Node types conflict here and returns a Timeout instead of a number. + // Casting to Function uses the DOM definition instead. + // TODO: Remove Node types from resolution here. + // eslint-disable-next-line @typescript-eslint/ban-types + flushTimeoutId = setTimeout(flush as Function, BUFFER_WAIT_TIME); + } +} + +export function flush() { + if (ANALYTICS_URL !== '') { + HTTPUtils.post(ANALYTICS_URL, [...EVENT_BUFFER]); + } + + EVENT_BUFFER = []; +} diff --git a/bundles/tracker/analytics/AnalyticsEvent.tsx b/bundles/tracker/analytics/AnalyticsEvent.tsx new file mode 100644 index 000000000..386cbdd4a --- /dev/null +++ b/bundles/tracker/analytics/AnalyticsEvent.tsx @@ -0,0 +1,6 @@ +export enum AnalyticsEvent { + TRACKER_APP_LOADED = 'tracker_app_loaded', + DONATE_FORM_VIEWED = 'donate_form_viewed', +} + +export default AnalyticsEvent; diff --git a/bundles/tracker/donation/DonationActions.ts b/bundles/tracker/donation/DonationActions.ts index e912b6716..6125d500e 100644 --- a/bundles/tracker/donation/DonationActions.ts +++ b/bundles/tracker/donation/DonationActions.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; -import { ActionTypes } from '../Action'; +import { ActionTypes } from '@tracker/Action'; + import { Bid, Donation, DonationFormErrors } from './DonationTypes'; export function loadDonation(donation: any, bids: Bid[], formErrors: DonationFormErrors) { @@ -83,7 +84,7 @@ export function submitDonation(donateUrl: string, csrfToken: string, donation: D const submissionData = buildDonationPayload(csrfToken, donation, bids); _.forEach(submissionData, (value, field) => { - const input = document.createElement('input'); + const input = document.createElement('textarea'); input.name = field; input.value = value.toString(); form.appendChild(input); diff --git a/bundles/tracker/donation/DonationReducer.ts b/bundles/tracker/donation/DonationReducer.ts index e5aad8834..d91cd9a4e 100644 --- a/bundles/tracker/donation/DonationReducer.ts +++ b/bundles/tracker/donation/DonationReducer.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import { Bid, Donation, DonationAction, DonationFormErrors } from './DonationTypes'; +import { ActionFor, ActionTypes } from '@tracker/Action'; -import { ActionFor, ActionTypes } from '../Action'; +import { Bid, Donation, DonationAction, DonationFormErrors } from './DonationTypes'; type DonationState = { donation: Donation; diff --git a/bundles/tracker/donation/DonationReducerSpec.ts b/bundles/tracker/donation/DonationReducerSpec.ts index be480df10..571de8195 100644 --- a/bundles/tracker/donation/DonationReducerSpec.ts +++ b/bundles/tracker/donation/DonationReducerSpec.ts @@ -1,5 +1,5 @@ -import DonationReducer from './DonationReducer'; import { createBid, deleteBid } from './DonationActions'; +import DonationReducer from './DonationReducer'; describe('DonationReducer', () => { let state: ReturnType; diff --git a/bundles/tracker/donation/DonationStore.ts b/bundles/tracker/donation/DonationStore.ts index daab7808b..8227f60c7 100644 --- a/bundles/tracker/donation/DonationStore.ts +++ b/bundles/tracker/donation/DonationStore.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; -import * as EventDetailsStore from '../event_details/EventDetailsStore'; -import { StoreState } from '../Store'; +import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore'; +import { StoreState } from '@tracker/Store'; + import validateDonationUtil from './validateDonation'; const getDonationState = (state: StoreState) => state.donation.donation; diff --git a/bundles/tracker/donation/__tests__/Donate.spec.tsx b/bundles/tracker/donation/__tests__/Donate.spec.tsx index b6b419eaf..9d463b10e 100644 --- a/bundles/tracker/donation/__tests__/Donate.spec.tsx +++ b/bundles/tracker/donation/__tests__/Donate.spec.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; + import userEvent from '@testing-library/user-event'; -import { createStore, fireEvent, render } from '../../testing/test-utils'; +import * as EventDetailsActions from '@tracker/event_details/EventDetailsActions'; +import { createStore, fireEvent, render } from '@tracker/testing/test-utils'; -import * as EventDetailsActions from '../../event_details/EventDetailsActions'; import Donate from '../components/Donate'; const renderDonate = (store?: ReturnType) => { diff --git a/bundles/tracker/donation/__tests__/DonationActions.spec.ts b/bundles/tracker/donation/__tests__/DonationActions.spec.ts index 9f586e329..485cbadea 100644 --- a/bundles/tracker/donation/__tests__/DonationActions.spec.ts +++ b/bundles/tracker/donation/__tests__/DonationActions.spec.ts @@ -1,5 +1,5 @@ -import { Donation } from '../DonationTypes'; import { buildDonationPayload } from '../DonationActions'; +import { Donation } from '../DonationTypes'; describe('DonationActions', () => { describe('buildDonationPayload', () => { diff --git a/bundles/tracker/donation/__tests__/validateBid.spec.ts b/bundles/tracker/donation/__tests__/validateBid.spec.ts index b43843943..b4334877e 100644 --- a/bundles/tracker/donation/__tests__/validateBid.spec.ts +++ b/bundles/tracker/donation/__tests__/validateBid.spec.ts @@ -1,4 +1,5 @@ -import { Incentive } from '../../event_details/EventDetailsTypes'; +import { Incentive } from '@tracker/event_details/EventDetailsTypes'; + import { BID_MINIMUM_AMOUNT } from '../DonationConstants'; import { Bid, Donation } from '../DonationTypes'; import validateBid, { BidErrors } from '../validateBid'; diff --git a/bundles/tracker/donation/components/Donate.tsx b/bundles/tracker/donation/components/Donate.tsx index b201c8168..6528b4dd6 100644 --- a/bundles/tracker/donation/components/Donate.tsx +++ b/bundles/tracker/donation/components/Donate.tsx @@ -1,27 +1,30 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import * as CurrencyUtils from '../../../public/util/currency'; -import Anchor from '../../../uikit/Anchor'; -import Button from '../../../uikit/Button'; -import Container from '../../../uikit/Container'; -import CurrencyInput from '../../../uikit/CurrencyInput'; -import ErrorAlert from '../../../uikit/ErrorAlert'; -import Header from '../../../uikit/Header'; -import Text from '../../../uikit/Text'; -import TextInput from '../../../uikit/TextInput'; -import useDispatch from '../../hooks/useDispatch'; -import * as EventDetailsStore from '../../event_details/EventDetailsStore'; -import { StoreState } from '../../Store'; +import { useConstants } from '@common/Constants'; +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import * as CurrencyUtils from '@public/util/currency'; +import Anchor from '@uikit/Anchor'; +import Button from '@uikit/Button'; +import Container from '@uikit/Container'; +import CurrencyInput from '@uikit/CurrencyInput'; +import ErrorAlert from '@uikit/ErrorAlert'; +import Header from '@uikit/Header'; +import Markdown from '@uikit/Markdown'; +import Text from '@uikit/Text'; +import TextInput from '@uikit/TextInput'; + +import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore'; +import useDispatch from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; + +import { AnalyticsEvent, track } from '../../analytics/Analytics'; import * as DonationActions from '../DonationActions'; +import { AMOUNT_PRESETS } from '../DonationConstants'; import * as DonationStore from '../DonationStore'; +import DonationIncentives from './DonationIncentives'; -import { AMOUNT_PRESETS } from '../DonationConstants'; import styles from './Donate.mod.css'; -import { useCachedCallback } from '../../../public/hooks/useCachedCallback'; -import { useConstants } from '../../../common/Constants'; -import DonationIncentives from './DonationIncentives'; -import Markdown from '../../../uikit/Markdown'; type DonateProps = { eventId: string | number; @@ -30,15 +33,28 @@ type DonateProps = { const Donate = (props: DonateProps) => { const { PRIVACY_POLICY_URL } = useConstants(); const dispatch = useDispatch(); + const { eventId } = props; + + const { eventDetails, prizes, donation, bids, commentErrors, donationValidity } = useSelector( + (state: StoreState) => ({ + eventDetails: EventDetailsStore.getEventDetails(state), + prizes: EventDetailsStore.getPrizes(state), + donation: DonationStore.getDonation(state), + bids: DonationStore.getBids(state), + commentErrors: DonationStore.getCommentFormErrors(state), + donationValidity: DonationStore.validateDonation(state), + }), + ); - const { eventDetails, donation, bids, donationValidity, commentErrors } = useSelector((state: StoreState) => ({ - eventDetails: EventDetailsStore.getEventDetails(state), - prizes: EventDetailsStore.getPrizes(state), - donation: DonationStore.getDonation(state), - bids: DonationStore.getBids(state), - commentErrors: DonationStore.getCommentFormErrors(state), - donationValidity: DonationStore.validateDonation(state), - })); + React.useEffect(() => { + track(AnalyticsEvent.DONATE_FORM_VIEWED, { + event_url_id: eventId, + prize_count: prizes.length, + bid_count: bids.length, + }); + // Only want to fire this event when the context of the page changes, not when data updates. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [eventId]); const { receiverName, donateUrl, minimumDonation, maximumDonation, step } = eventDetails; const { name, email, amount, comment } = donation; diff --git a/bundles/tracker/donation/components/DonateInitializer.tsx b/bundles/tracker/donation/components/DonateInitializer.tsx index e2ee60c1f..2cb454c29 100644 --- a/bundles/tracker/donation/components/DonateInitializer.tsx +++ b/bundles/tracker/donation/components/DonateInitializer.tsx @@ -1,13 +1,15 @@ import * as React from 'react'; import _ from 'lodash'; -import * as CurrencyUtils from '../../../public/util/currency'; -import * as EventDetailsActions from '../../event_details/EventDetailsActions'; -import { Prize } from '../../event_details/EventDetailsTypes'; -import useDispatch from '../../hooks/useDispatch'; +import * as CurrencyUtils from '@public/util/currency'; + +import * as EventDetailsActions from '@tracker/event_details/EventDetailsActions'; +import { Prize } from '@tracker/event_details/EventDetailsTypes'; +import useDispatch from '@tracker/hooks/useDispatch'; +import RouterUtils from '@tracker/router/RouterUtils'; + import * as DonationActions from '../DonationActions'; import { Bid, DonationFormErrors } from '../DonationTypes'; -import RouterUtils from '../../router/RouterUtils'; /* DonateInitializer acts as a proxy for bringing the preloaded props provided diff --git a/bundles/tracker/donation/components/DonationBidForm.tsx b/bundles/tracker/donation/components/DonationBidForm.tsx index 93344d3c9..80b733954 100644 --- a/bundles/tracker/donation/components/DonationBidForm.tsx +++ b/bundles/tracker/donation/components/DonationBidForm.tsx @@ -1,23 +1,25 @@ import * as React from 'react'; -import { useSelector } from 'react-redux'; import classNames from 'classnames'; +import { useSelector } from 'react-redux'; + +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import * as CurrencyUtils from '@public/util/currency'; +import Button from '@uikit/Button'; +import Checkbox from '@uikit/Checkbox'; +import CurrencyInput from '@uikit/CurrencyInput'; +import Header from '@uikit/Header'; +import ProgressBar from '@uikit/ProgressBar'; +import Text from '@uikit/Text'; +import TextInput from '@uikit/TextInput'; + +import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore'; +import { StoreState } from '@tracker/Store'; -import * as CurrencyUtils from '../../../public/util/currency'; -import { StoreState } from '../../Store'; -import Button from '../../../uikit/Button'; -import Checkbox from '../../../uikit/Checkbox'; -import CurrencyInput from '../../../uikit/CurrencyInput'; -import Header from '../../../uikit/Header'; -import ProgressBar from '../../../uikit/ProgressBar'; -import Text from '../../../uikit/Text'; -import TextInput from '../../../uikit/TextInput'; -import * as EventDetailsStore from '../../event_details/EventDetailsStore'; import * as DonationStore from '../DonationStore'; import { Bid } from '../DonationTypes'; import validateBid from '../validateBid'; import styles from './DonationBidForm.mod.css'; -import { useCachedCallback } from '../../../public/hooks/useCachedCallback'; type DonationBidFormProps = { incentiveId: number; @@ -46,14 +48,6 @@ const DonationBidForm = (props: DonationBidFormProps) => { const [customOptionSelected, setCustomOptionSelected] = React.useState(false); const [customOption, setCustomOption] = React.useState(''); - // When the selected incentive changes, reset the form fields - React.useEffect(() => { - setAllocatedAmount(remainingDonationTotal); - setSelectedChoiceId(undefined); - setCustomOptionSelected(false); - setCustomOption(''); - }, [incentiveId]); - const bidValidation = React.useMemo( () => validateBid( @@ -69,7 +63,17 @@ const DonationBidForm = (props: DonationBidFormProps) => { selectedChoiceId != null, customOptionSelected, ), - [selectedChoiceId, allocatedAmount, customOption, incentive, donation, bids, bidChoices, customOptionSelected], + [ + selectedChoiceId, + incentiveId, + allocatedAmount, + customOption, + incentive, + donation, + bids, + bidChoices.length, + customOptionSelected, + ], ); const handleNewChoice = useCachedCallback(choiceId => { diff --git a/bundles/tracker/donation/components/DonationBids.tsx b/bundles/tracker/donation/components/DonationBids.tsx index fc38052a7..59603d634 100644 --- a/bundles/tracker/donation/components/DonationBids.tsx +++ b/bundles/tracker/donation/components/DonationBids.tsx @@ -2,21 +2,23 @@ import * as React from 'react'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import useDispatch from '../../hooks/useDispatch'; -import * as CurrencyUtils from '../../../public/util/currency'; -import { StoreState } from '../../Store'; -import Button from '../../../uikit/Button'; -import Header from '../../../uikit/Header'; -import Text from '../../../uikit/Text'; -import * as EventDetailsStore from '../../event_details/EventDetailsStore'; -import { Incentive } from '../../event_details/EventDetailsTypes'; +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import * as CurrencyUtils from '@public/util/currency'; +import Button from '@uikit/Button'; +import ErrorAlert from '@uikit/ErrorAlert'; +import Header from '@uikit/Header'; +import Text from '@uikit/Text'; + +import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore'; +import { Incentive } from '@tracker/event_details/EventDetailsTypes'; +import useDispatch from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; + import * as DonationActions from '../DonationActions'; import * as DonationStore from '../DonationStore'; import { Bid, BidFormErrors } from '../DonationTypes'; import styles from './DonationBids.mod.css'; -import ErrorAlert from '../../../uikit/ErrorAlert'; -import { useCachedCallback } from '../../../public/hooks/useCachedCallback'; type BidItemProps = { bid: Bid; diff --git a/bundles/tracker/donation/components/DonationIncentives.tsx b/bundles/tracker/donation/components/DonationIncentives.tsx index e86110e40..d782a4c5a 100644 --- a/bundles/tracker/donation/components/DonationIncentives.tsx +++ b/bundles/tracker/donation/components/DonationIncentives.tsx @@ -1,24 +1,26 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import _ from 'lodash'; import classNames from 'classnames'; +import _ from 'lodash'; +import { useSelector } from 'react-redux'; + +import { useCachedCallback } from '@public/hooks/useCachedCallback'; +import Button from '@uikit/Button'; +import Clickable from '@uikit/Clickable'; +import Header from '@uikit/Header'; +import Text from '@uikit/Text'; +import TextInput from '@uikit/TextInput'; + +import * as DonationStore from '@tracker/donation/DonationStore'; +import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore'; +import searchIncentives from '@tracker/event_details/searchIncentives'; +import useDispatch from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; -import Button from '../../../uikit/Button'; -import Clickable from '../../../uikit/Clickable'; -import Header from '../../../uikit/Header'; -import Text from '../../../uikit/Text'; -import TextInput from '../../../uikit/TextInput'; -import useDispatch from '../../hooks/useDispatch'; -import { StoreState } from '../../Store'; -import * as EventDetailsStore from '../../event_details/EventDetailsStore'; -import searchIncentives from '../../event_details/searchIncentives'; import * as DonationActions from '../DonationActions'; -import * as DonationStore from '../../donation/DonationStore'; import DonationBidForm from './DonationBidForm'; import DonationBids from './DonationBids'; import styles from './DonationIncentives.mod.css'; -import { useCachedCallback } from '../../../public/hooks/useCachedCallback'; type DonationIncentivesProps = { step: number; @@ -90,6 +92,7 @@ const DonationIncentives = (props: DonationIncentivesProps) => { {selectedIncentiveId != null ? ( state.eventDetails; diff --git a/bundles/tracker/event_details/EventDetailsStoreSpec.ts b/bundles/tracker/event_details/EventDetailsStoreSpec.ts index 3312a20d8..d6d44aae8 100644 --- a/bundles/tracker/event_details/EventDetailsStoreSpec.ts +++ b/bundles/tracker/event_details/EventDetailsStoreSpec.ts @@ -1,6 +1,6 @@ -import { getTopLevelIncentives } from './EventDetailsStore'; -import { combinedReducer, StoreState } from '../Store'; import { getFixtureBid } from '../../../spec/fixtures/bid'; +import { combinedReducer, StoreState } from '../Store'; +import { getTopLevelIncentives } from './EventDetailsStore'; describe('EventDetailsStore', () => { const bid1 = getFixtureBid({ id: 1, order: 50 }); diff --git a/bundles/tracker/events/EventActions.ts b/bundles/tracker/events/EventActions.ts index ad69b4ba5..325868c5a 100644 --- a/bundles/tracker/events/EventActions.ts +++ b/bundles/tracker/events/EventActions.ts @@ -1,10 +1,12 @@ -import { ActionTypes } from '../Action'; -import { SafeDispatch } from '../hooks/useDispatch'; -import * as CurrencyUtils from '../../public/util/currency'; -import * as HTTPUtils from '../../public/util/http'; -import TimeUtils from '../../public/util/TimeUtils'; -import { Event, EventSearchFilter } from './EventTypes'; +import * as CurrencyUtils from '@public/util/currency'; +import * as HTTPUtils from '@public/util/http'; +import TimeUtils from '@public/util/TimeUtils'; + +import { ActionTypes } from '@tracker/Action'; +import { SafeDispatch } from '@tracker/hooks/useDispatch'; + import Endpoints from '../Endpoints'; +import { Event, EventSearchFilter } from './EventTypes'; function eventFromAPIEvent({ pk, fields }: { pk: number; fields: { [field: string]: any } }): Event { return { diff --git a/bundles/tracker/events/EventActionsSpec.ts b/bundles/tracker/events/EventActionsSpec.ts index bc51066a5..aa1b5d62a 100644 --- a/bundles/tracker/events/EventActionsSpec.ts +++ b/bundles/tracker/events/EventActionsSpec.ts @@ -1,14 +1,14 @@ import fetchMock from 'fetch-mock'; -import { fetchEvents } from './EventActions'; -import { AnyAction } from 'redux'; -import thunk, { ThunkDispatch } from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; -import { StoreState } from '../Store'; -import Endpoints from '../Endpoints'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; +import { SafeDispatch } from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; -type DispatchExts = ThunkDispatch; +import { fetchEvents } from './EventActions'; -const mockStore = configureMockStore([thunk]); +const mockStore = configureMockStore([thunk]); describe('EventActions', () => { let store: ReturnType; diff --git a/bundles/tracker/events/EventReducer.ts b/bundles/tracker/events/EventReducer.ts index 46c53f415..b5bb9a4cb 100644 --- a/bundles/tracker/events/EventReducer.ts +++ b/bundles/tracker/events/EventReducer.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; -import { ActionFor, ActionTypes } from '../Action'; +import { ActionFor, ActionTypes } from '@tracker/Action'; + import { Event, EventAction } from './EventTypes'; type EventsState = { diff --git a/bundles/tracker/events/EventStore.ts b/bundles/tracker/events/EventStore.ts index 4d0b71571..b096bc0de 100644 --- a/bundles/tracker/events/EventStore.ts +++ b/bundles/tracker/events/EventStore.ts @@ -1,7 +1,7 @@ -import { createSelector } from 'reselect'; import createCachedSelector from 're-reselect'; +import { createSelector } from 'reselect'; -import { StoreState } from '../Store'; +import { StoreState } from '@tracker/Store'; const getEventsState = (state: StoreState) => state.events; const getEventId = (state: StoreState, { eventId }: { eventId: string }) => { diff --git a/bundles/tracker/events/EventStoreSpec.ts b/bundles/tracker/events/EventStoreSpec.ts index e60a23cca..5923ee9ec 100644 --- a/bundles/tracker/events/EventStoreSpec.ts +++ b/bundles/tracker/events/EventStoreSpec.ts @@ -1,6 +1,7 @@ -import { getEvent } from './EventStore'; -import { combinedReducer, StoreState } from '../Store'; +import { combinedReducer, StoreState } from '@tracker/Store'; + import { getFixtureEvent } from '../../../spec/fixtures/event'; +import { getEvent } from './EventStore'; describe('EventStore', () => { const event = getFixtureEvent(); diff --git a/bundles/tracker/events/EventTypes.ts b/bundles/tracker/events/EventTypes.ts index 72eb08c64..b612159fd 100644 --- a/bundles/tracker/events/EventTypes.ts +++ b/bundles/tracker/events/EventTypes.ts @@ -1,4 +1,4 @@ -import { DateTime } from '../../public/util/TimeUtils'; +import { DateTime } from '@public/util/TimeUtils'; export type Event = { id: string; diff --git a/bundles/tracker/events/components/EventRouter.tsx b/bundles/tracker/events/components/EventRouter.tsx index 0d885ec26..1eaf333c2 100644 --- a/bundles/tracker/events/components/EventRouter.tsx +++ b/bundles/tracker/events/components/EventRouter.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import Donate from '../../donation/components/Donate'; -import DonateInitializer from '../../donation/components/DonateInitializer'; -import Prize from '../../prizes/components/Prize'; -import Prizes from '../../prizes/components/Prizes'; -import { Routes } from '../../router/RouterUtils'; -import NotFound from '../../router/components/NotFound'; -import { useConstants } from '../../../common/Constants'; +import { useConstants } from '@common/Constants'; + +import Donate from '@tracker/donation/components/Donate'; +import DonateInitializer from '@tracker/donation/components/DonateInitializer'; +import Prize from '@tracker/prizes/components/Prize'; +import Prizes from '@tracker/prizes/components/Prizes'; +import NotFound from '@tracker/router/components/NotFound'; +import { Routes } from '@tracker/router/RouterUtils'; const EventRouter = (props: any) => { // TODO: type this better when DonateInitializer doesn't need page-load props diff --git a/bundles/tracker/hooks/useDispatch.ts b/bundles/tracker/hooks/useDispatch.ts index 8fd5b0c62..1f7d24a2b 100644 --- a/bundles/tracker/hooks/useDispatch.ts +++ b/bundles/tracker/hooks/useDispatch.ts @@ -1,7 +1,8 @@ import { useDispatch } from 'react-redux'; import { ThunkDispatch } from 'redux-thunk'; -import { Action } from '../Action'; -import { StoreState } from '../Store'; + +import { Action } from '@tracker/Action'; +import { StoreState } from '@tracker/Store'; const useSafeDispatch = () => useDispatch>(); diff --git a/bundles/tracker/index.tsx b/bundles/tracker/index.tsx index dc9cea0ff..a3c6d3f74 100644 --- a/bundles/tracker/index.tsx +++ b/bundles/tracker/index.tsx @@ -2,13 +2,14 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import '../common/init'; +import Constants from '@common/Constants'; +import ErrorBoundary from '@public/errorBoundary'; +import ThemeProvider from '@uikit/ThemeProvider'; -import ErrorBoundary from '../public/errorBoundary'; -import ThemeProvider from '../uikit/ThemeProvider'; import AppWrapper from './App'; import { createTrackerStore } from './Store'; -import Constants from '../common/Constants'; + +import '@common/init'; // TODO: Migrate all page-load props to API calls. Currently these props // are just being proxied through to `AppWrapper` which decides what props diff --git a/bundles/tracker/prizes/PrizeActions.ts b/bundles/tracker/prizes/PrizeActions.ts index 115f4b12d..2effa41d8 100644 --- a/bundles/tracker/prizes/PrizeActions.ts +++ b/bundles/tracker/prizes/PrizeActions.ts @@ -1,13 +1,15 @@ import _ from 'lodash'; -import { ActionTypes } from '../Action'; -import { SafeDispatch } from '../hooks/useDispatch'; -import * as CurrencyUtils from '../../public/util/currency'; -import * as HTTPUtils from '../../public/util/http'; -import TimeUtils from '../../public/util/TimeUtils'; -import { Run } from '../runs/RunTypes'; +import * as CurrencyUtils from '@public/util/currency'; +import * as HTTPUtils from '@public/util/http'; +import TimeUtils from '@public/util/TimeUtils'; + +import { ActionTypes } from '@tracker/Action'; +import Endpoints from '@tracker/Endpoints'; +import { SafeDispatch } from '@tracker/hooks/useDispatch'; +import { Run } from '@tracker/runs/RunTypes'; + import { Prize, PrizeSearchFilter } from './PrizeTypes'; -import Endpoints from '../Endpoints'; function runFromNestedAPIRun(prefix: string, fields: { [field: string]: any }): Run | undefined { const runFields: { [field: string]: any } = {}; diff --git a/bundles/tracker/prizes/PrizeActionsSpec.ts b/bundles/tracker/prizes/PrizeActionsSpec.ts index 39adfbb32..dc197c96e 100644 --- a/bundles/tracker/prizes/PrizeActionsSpec.ts +++ b/bundles/tracker/prizes/PrizeActionsSpec.ts @@ -1,14 +1,14 @@ import fetchMock from 'fetch-mock'; -import thunk, { ThunkDispatch } from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; -import { fetchPrizes } from './PrizeActions'; -import { AnyAction } from 'redux'; -import { StoreState } from '../Store'; -import Endpoints from '../Endpoints'; +import thunk from 'redux-thunk'; + +import Endpoints from '@tracker/Endpoints'; +import { SafeDispatch } from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; -type DispatchExts = ThunkDispatch; +import { fetchPrizes } from './PrizeActions'; -const mockStore = configureMockStore([thunk]); +const mockStore = configureMockStore([thunk]); describe('PrizeActions', () => { let store: ReturnType; diff --git a/bundles/tracker/prizes/PrizeReducer.ts b/bundles/tracker/prizes/PrizeReducer.ts index ceef09813..242eff8c3 100644 --- a/bundles/tracker/prizes/PrizeReducer.ts +++ b/bundles/tracker/prizes/PrizeReducer.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; -import { ActionFor, ActionTypes } from '../Action'; +import { ActionFor, ActionTypes } from '@tracker/Action'; + import { Prize, PrizeAction } from './PrizeTypes'; type PrizesState = { diff --git a/bundles/tracker/prizes/PrizeStore.ts b/bundles/tracker/prizes/PrizeStore.ts index 21bcf6445..271589b09 100644 --- a/bundles/tracker/prizes/PrizeStore.ts +++ b/bundles/tracker/prizes/PrizeStore.ts @@ -1,9 +1,11 @@ -import { createSelector } from 'reselect'; -import createCachedSelector from 're-reselect'; import _ from 'lodash'; +import createCachedSelector from 're-reselect'; +import { createSelector } from 'reselect'; + +import TimeUtils, { DateTime, Duration, Interval } from '@public/util/TimeUtils'; + +import { StoreState } from '@tracker/Store'; -import TimeUtils, { DateTime, Duration, Interval } from '../../public/util/TimeUtils'; -import { StoreState } from '../Store'; import { Prize } from './PrizeTypes'; const SOON_DURATION = Duration.fromMillis(7 * 60 * 60 * 1000); // 4 hours diff --git a/bundles/tracker/prizes/PrizeTypes.ts b/bundles/tracker/prizes/PrizeTypes.ts index be4f11227..eb1fcf957 100644 --- a/bundles/tracker/prizes/PrizeTypes.ts +++ b/bundles/tracker/prizes/PrizeTypes.ts @@ -1,5 +1,6 @@ -import { DateTime } from '../../public/util/TimeUtils'; -import { Run } from '../runs/RunTypes'; +import { DateTime } from '@public/util/TimeUtils'; + +import { Run } from '@tracker/runs/RunTypes'; export type PrizeCategory = { name: string; diff --git a/bundles/tracker/prizes/PrizeUtils.ts b/bundles/tracker/prizes/PrizeUtils.ts index eafce8dc8..e5dfc5972 100644 --- a/bundles/tracker/prizes/PrizeUtils.ts +++ b/bundles/tracker/prizes/PrizeUtils.ts @@ -1,6 +1,7 @@ -import { Prize } from './PrizeTypes'; import _ from 'lodash'; +import { Prize } from './PrizeTypes'; + /** * Returns the URL of an image to use when showing a Prize individually. */ diff --git a/bundles/tracker/prizes/__tests__/getPrizeRelativeAvailability.spec.ts b/bundles/tracker/prizes/__tests__/getPrizeRelativeAvailability.spec.ts index 1107314ae..95ae8a609 100644 --- a/bundles/tracker/prizes/__tests__/getPrizeRelativeAvailability.spec.ts +++ b/bundles/tracker/prizes/__tests__/getPrizeRelativeAvailability.spec.ts @@ -1,12 +1,10 @@ -import * as React from 'react'; -import { createStore, render } from '../../testing/test-utils'; +import TimeUtils, { DateTime } from '@public/util/TimeUtils'; + +import { Run } from '@tracker/runs/RunTypes'; -import TimeUtils, { DateTime } from '../../../public/util/TimeUtils'; -import * as EventDetailsActions from '../../event_details/EventDetailsActions'; -import { Run } from '../../runs/RunTypes'; import getPrizeRelativeAvailability, { - EXACT_TIME_RELATIVE_LIMIT, ALLOWED_ESTIMATED_TIMES, + EXACT_TIME_RELATIVE_LIMIT, } from '../getPrizeRelativeAvailability'; import { Prize } from '../PrizeTypes'; diff --git a/bundles/tracker/prizes/components/Prize.tsx b/bundles/tracker/prizes/components/Prize.tsx index 74e025c1c..cddeaabbc 100644 --- a/bundles/tracker/prizes/components/Prize.tsx +++ b/bundles/tracker/prizes/components/Prize.tsx @@ -1,25 +1,27 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import * as CurrencyUtils from '../../../public/util/currency'; -import TimeUtils, { DateTime } from '../../../public/util/TimeUtils'; -import Anchor from '../../../uikit/Anchor'; -import Button from '../../../uikit/Button'; -import Container from '../../../uikit/Container'; -import Header from '../../../uikit/Header'; -import LoadingDots from '../../../uikit/LoadingDots'; -import Markdown from '../../../uikit/Markdown'; -import Text from '../../../uikit/Text'; -import useDispatch from '../../hooks/useDispatch'; -import * as EventActions from '../../events/EventActions'; -import * as EventStore from '../../events/EventStore'; -import RouterUtils, { Routes } from '../../router/RouterUtils'; -import { StoreState } from '../../Store'; +import { useConstants } from '@common/Constants'; +import * as CurrencyUtils from '@public/util/currency'; +import TimeUtils, { DateTime } from '@public/util/TimeUtils'; +import Anchor from '@uikit/Anchor'; +import Button from '@uikit/Button'; +import Container from '@uikit/Container'; +import Header from '@uikit/Header'; +import LoadingDots from '@uikit/LoadingDots'; +import Markdown from '@uikit/Markdown'; +import Text from '@uikit/Text'; + +import * as EventActions from '@tracker/events/EventActions'; +import * as EventStore from '@tracker/events/EventStore'; +import useDispatch from '@tracker/hooks/useDispatch'; +import RouterUtils, { Routes } from '@tracker/router/RouterUtils'; +import { StoreState } from '@tracker/Store'; + import * as PrizeActions from '../PrizeActions'; import * as PrizeStore from '../PrizeStore'; import * as PrizeTypes from '../PrizeTypes'; import * as PrizeUtils from '../PrizeUtils'; -import { useConstants } from '../../../common/Constants'; import styles from './Prize.mod.css'; @@ -101,7 +103,7 @@ const Prize = (props: PrizeProps) => { useEffect(() => { dispatch(PrizeActions.fetchPrizes({ id: prizeId })); - }, [dispatch]); + }, [dispatch, prizeId]); useEffect(() => { if (event == null && eventId != null) { diff --git a/bundles/tracker/prizes/components/PrizeCard.tsx b/bundles/tracker/prizes/components/PrizeCard.tsx index 458fdb290..bcf930ab3 100644 --- a/bundles/tracker/prizes/components/PrizeCard.tsx +++ b/bundles/tracker/prizes/components/PrizeCard.tsx @@ -1,17 +1,19 @@ -import React, { useState, useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import * as CurrencyUtils from '../../../public/util/currency'; -import TimeUtils from '../../../public/util/TimeUtils'; -import Button from '../../../uikit/Button'; -import Clickable from '../../../uikit/Clickable'; -import Header from '../../../uikit/Header'; -import Text from '../../../uikit/Text'; -import { StoreState } from '../../Store'; -import RouterUtils, { Routes } from '../../router/RouterUtils'; -import * as PrizeStore from '../PrizeStore'; +import * as CurrencyUtils from '@public/util/currency'; +import TimeUtils from '@public/util/TimeUtils'; +import Button from '@uikit/Button'; +import Clickable from '@uikit/Clickable'; +import Header from '@uikit/Header'; +import Text from '@uikit/Text'; + +import RouterUtils, { Routes } from '@tracker/router/RouterUtils'; +import { StoreState } from '@tracker/Store'; + import getPrizeRelativeAvailability from '../getPrizeRelativeAvailability'; +import * as PrizeStore from '../PrizeStore'; import * as PrizeUtils from '../PrizeUtils'; import styles from './PrizeCard.mod.css'; diff --git a/bundles/tracker/prizes/components/PrizeCardSpec.tsx b/bundles/tracker/prizes/components/PrizeCardSpec.tsx index 7f246c731..ba797e769 100644 --- a/bundles/tracker/prizes/components/PrizeCardSpec.tsx +++ b/bundles/tracker/prizes/components/PrizeCardSpec.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import PrizeCard from './PrizeCard'; -import { mockState, mountWithState } from '../../../../spec/Suite'; + import { getFixturePrize } from '../../../../spec/fixtures/Prize'; +import { mockState, mountWithState } from '../../../../spec/Suite'; +import PrizeCard from './PrizeCard'; describe('PrizeCard', () => { let subject; diff --git a/bundles/tracker/prizes/components/PrizeSpec.tsx b/bundles/tracker/prizes/components/PrizeSpec.tsx index baf0148c3..f5c96330f 100644 --- a/bundles/tracker/prizes/components/PrizeSpec.tsx +++ b/bundles/tracker/prizes/components/PrizeSpec.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import Prize from './Prize'; -import { mockState, mountWithState } from '../../../../spec/Suite'; + import { getFixturePrize } from '../../../../spec/fixtures/Prize'; +import { mockState, mountWithState } from '../../../../spec/Suite'; +import Prize from './Prize'; describe('Prize', () => { let subject; diff --git a/bundles/tracker/prizes/components/Prizes.tsx b/bundles/tracker/prizes/components/Prizes.tsx index bb17f743c..fd9c4f575 100644 --- a/bundles/tracker/prizes/components/Prizes.tsx +++ b/bundles/tracker/prizes/components/Prizes.tsx @@ -1,23 +1,25 @@ import * as React from 'react'; import { useSelector } from 'react-redux'; -import TimeUtils from '../../../public/util/TimeUtils'; -import Container from '../../../uikit/Container'; -import Header from '../../../uikit/Header'; -import LoadingDots from '../../../uikit/LoadingDots'; -import * as EventActions from '../../events/EventActions'; -import * as EventStore from '../../events/EventStore'; -import useDispatch from '../../hooks/useDispatch'; -import { StoreState } from '../../Store'; +import { useConstants } from '@common/Constants'; +import TimeUtils from '@public/util/TimeUtils'; +import Anchor from '@uikit/Anchor'; +import Container from '@uikit/Container'; +import Header from '@uikit/Header'; +import LoadingDots from '@uikit/LoadingDots'; +import Text from '@uikit/Text'; + +import * as EventActions from '@tracker/events/EventActions'; +import * as EventStore from '@tracker/events/EventStore'; +import useDispatch from '@tracker/hooks/useDispatch'; +import { StoreState } from '@tracker/Store'; + import * as PrizeActions from '../PrizeActions'; import * as PrizeStore from '../PrizeStore'; -import PrizeCard from './PrizeCard'; import { Prize } from '../PrizeTypes'; +import PrizeCard from './PrizeCard'; import styles from './Prizes.mod.css'; -import Text from '../../../uikit/Text'; -import Anchor from '../../../uikit/Anchor'; -import { useConstants } from '../../../common/Constants'; // The limit of how many prizes should be included in sections that appear // above the All Prizes section. This generally avoids showing prizes multiple @@ -69,12 +71,12 @@ const Prizes = (props: PrizesProps) => { React.useEffect(() => { setLoadingPrizes(true); dispatch(PrizeActions.fetchPrizes({ event: eventId })).finally(() => setLoadingPrizes(false)); - }, [eventId]); + }, [dispatch, eventId]); React.useEffect(() => { if (event != null) return; dispatch(EventActions.fetchEvents({ id: eventId })); - }, [event, eventId]); + }, [dispatch, event, eventId]); if (event == null) { return ( diff --git a/bundles/tracker/prizes/getPrizeRelativeAvailability.ts b/bundles/tracker/prizes/getPrizeRelativeAvailability.ts index 8ed1a1d85..a46f93a48 100644 --- a/bundles/tracker/prizes/getPrizeRelativeAvailability.ts +++ b/bundles/tracker/prizes/getPrizeRelativeAvailability.ts @@ -1,7 +1,8 @@ import * as React from 'react'; import _ from 'lodash'; -import { DateTime } from '../../public/util/TimeUtils'; +import { DateTime } from '@public/util/TimeUtils'; + import { Prize } from './PrizeTypes'; // Because timestamps are estimates at best, we only want to show relative diff --git a/bundles/tracker/router/RouterUtils.ts b/bundles/tracker/router/RouterUtils.ts index b18cc4c45..609444778 100644 --- a/bundles/tracker/router/RouterUtils.ts +++ b/bundles/tracker/router/RouterUtils.ts @@ -1,6 +1,6 @@ +import React from 'react'; import { createBrowserHistory } from 'history'; import queryString from 'query-string'; -import React from 'react'; export const Routes = { EVENT_BASE: (eventId: string | number) => `/events/${eventId}`, diff --git a/bundles/tracker/router/components/NotFound.tsx b/bundles/tracker/router/components/NotFound.tsx index 7dea8d51f..22821648e 100644 --- a/bundles/tracker/router/components/NotFound.tsx +++ b/bundles/tracker/router/components/NotFound.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import Container from '../../../uikit/Container'; -import Header from '../../../uikit/Header'; -import Text from '../../../uikit/Text'; +import Container from '@uikit/Container'; +import Header from '@uikit/Header'; +import Text from '@uikit/Text'; const NotFound = () => { return ( diff --git a/bundles/tracker/runs/RunTypes.ts b/bundles/tracker/runs/RunTypes.ts index f00c74ffa..bb9a6a809 100644 --- a/bundles/tracker/runs/RunTypes.ts +++ b/bundles/tracker/runs/RunTypes.ts @@ -1,4 +1,4 @@ -import { DateTime } from '../../public/util/TimeUtils'; +import { DateTime } from '@public/util/TimeUtils'; export type Run = { id?: string; diff --git a/bundles/tracker/testing/test-utils.tsx b/bundles/tracker/testing/test-utils.tsx index f771229f9..592e1eb48 100644 --- a/bundles/tracker/testing/test-utils.tsx +++ b/bundles/tracker/testing/test-utils.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; +import { createStore } from 'redux'; -import { combinedReducer, StoreState } from '../Store'; -import ThemeProvider from '../../uikit/ThemeProvider'; +import ThemeProvider from '@uikit/ThemeProvider'; + +import { render } from '@testing-library/react'; +import { combinedReducer, StoreState } from '@tracker/Store'; type WrapProps = { children: React.ReactNode; diff --git a/bundles/uikit/Anchor.tsx b/bundles/uikit/Anchor.tsx index 845ec1e6b..1f6a2fd1f 100644 --- a/bundles/uikit/Anchor.tsx +++ b/bundles/uikit/Anchor.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Link } from 'react-router-dom'; import classNames from 'classnames'; +import { Link } from 'react-router-dom'; import styles from './Anchor.mod.css'; diff --git a/bundles/uikit/Checkbox.tsx b/bundles/uikit/Checkbox.tsx index f529ad09f..e87f71cfe 100644 --- a/bundles/uikit/Checkbox.tsx +++ b/bundles/uikit/Checkbox.tsx @@ -54,7 +54,7 @@ const Checkbox = (props: CheckboxProps) => { const handleClick = React.useCallback(() => { onChange(!checked); - }, [checked]); + }, [checked, onChange]); return ( { (_event, value) => { onChange != null && onChange(value, name); }, - [name, value, onChange], + [name, onChange], ); return ( diff --git a/bundles/uikit/ErrorAlert.tsx b/bundles/uikit/ErrorAlert.tsx index 8fcfa28fa..9b3809f2e 100644 --- a/bundles/uikit/ErrorAlert.tsx +++ b/bundles/uikit/ErrorAlert.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import Alert from './Alert'; import Text from './Text'; diff --git a/bundles/uikit/IconTypes.tsx b/bundles/uikit/IconTypes.tsx index 141535caa..9d1d24e98 100644 --- a/bundles/uikit/IconTypes.tsx +++ b/bundles/uikit/IconTypes.tsx @@ -1,5 +1,5 @@ -import CheckboxOpen from './icons/CheckboxOpen'; import CheckboxChecked from './icons/CheckboxChecked'; +import CheckboxOpen from './icons/CheckboxOpen'; import Exclamation from './icons/Exclamation'; export const IconTypes = { diff --git a/bundles/uikit/icons/CheckboxChecked.tsx b/bundles/uikit/icons/CheckboxChecked.tsx index cf7d30f00..2a6dc7a94 100644 --- a/bundles/uikit/icons/CheckboxChecked.tsx +++ b/bundles/uikit/icons/CheckboxChecked.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + import { faCheckSquare } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; const CheckboxOpen = (props: any) => { return ; diff --git a/bundles/uikit/icons/CheckboxOpen.tsx b/bundles/uikit/icons/CheckboxOpen.tsx index f9dc4857e..4f084a9ab 100644 --- a/bundles/uikit/icons/CheckboxOpen.tsx +++ b/bundles/uikit/icons/CheckboxOpen.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + import { faSquare } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; const CheckboxOpen = (props: any) => { return ; diff --git a/bundles/uikit/icons/Exclamation.tsx b/bundles/uikit/icons/Exclamation.tsx index 023232d4f..15d67e90f 100644 --- a/bundles/uikit/icons/Exclamation.tsx +++ b/bundles/uikit/icons/Exclamation.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + import { faExclamation } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; const Exclamation = (props: any) => { return ; diff --git a/package.json b/package.json index a55a91c37..77b407508 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "history": "^4.10.1", "keymirror": "^0.1.1", "lodash": "^4.17.15", - "luxon": "^1.21.3", + "luxon": "^2.4.0", "memoize-one": "^5.0.4", "mini-css-extract-plugin": "^0.8.0", "moment": "^2.24.0", @@ -104,7 +104,7 @@ "@types/karma-webpack": "^2.0.7", "@types/keymirror": "^0.1.1", "@types/lodash": "^4.14.144", - "@types/luxon": "^1.21.0", + "@types/luxon": "^2.3.2", "@types/moment-timezone": "^0.5.12", "@types/prop-types": "^15.7.3", "@types/react": "^16.9.11", @@ -123,6 +123,7 @@ "eslint": "^7.5.0", "eslint-plugin-react": "^7.16.0", "eslint-plugin-react-hooks": "^4.0.8", + "eslint-plugin-simple-import-sort": "^7.0.0", "fetch-mock": "^9.10.7", "jasmine-core": "^3.5.0", "karma": "^4.4.1", @@ -133,7 +134,7 @@ "puppeteer": "^2.0.0", "react-hot-loader": "^4.12.15", "redux-mock-store": "^1.5.3", - "typescript": "^3.9.7", + "typescript": "^4.7.2", "webpack-dev-server": "^3.9.0" }, "resolve": { diff --git a/runtests.py b/runtests.py index 74e1bc17d..3421d90cb 100644 --- a/runtests.py +++ b/runtests.py @@ -1,10 +1,9 @@ #!/usr/bin/env python import os +import subprocess import sys import logging -import json from argparse import ArgumentParser -from subprocess import check_call import django from django.conf import settings @@ -46,12 +45,18 @@ default=False, help='Tells Django to stop running the test suite after first failed test.', ) - # TODO: the fetches for the ui endpoints blow up if the manifest doesn't exist so we have to build the webpack bundles first try: - json.load(open('tracker/ui-tracker.manifest.json'))['files']['tracker'] - except (IOError, KeyError): - check_call(['yarn', '--frozen-lockfile', '--production']) - check_call(['yarn', 'build']) + subprocess.check_call( + ['yarn', 'build'], + env={**os.environ, 'NODE_ENV': 'development', 'NO_HMR': '1'}, + ) + except subprocess.SubprocessError: + # maybe failed because the modules aren't installed + subprocess.check_call(['yarn', '--frozen-lockfile', '--production']) + subprocess.check_call( + ['yarn', 'build'], + env={**os.environ, 'NODE_ENV': 'development', 'NO_HMR': '1'}, + ) TestRunner = get_runner(settings, 'xmlrunner.extra.djangotestrunner.XMLTestRunner') TestRunner.add_arguments(parser) parsed = parser.parse_args(sys.argv[1:]) diff --git a/setup.py b/setup.py index 5078b8f05..4bb6e0602 100644 --- a/setup.py +++ b/setup.py @@ -55,19 +55,20 @@ def run(self): install_requires=[ 'celery~=5.0', 'channels>=2.0', - 'Django~=2.2', + 'Django>=2.2,!=3.0.*,!=3.1.*,<4.0', 'django-ajax-selects~=1.9', 'django-ical~=1.7', 'django-mptt~=0.10', - 'django-paypal @ git+https://github.com/cma2819/django-paypal.git', + 'django-paypal~=1.1', 'django-post-office~=3.2', - 'django-timezone-field>=3.1,<5.0', + 'django-timezone-field>=3.1,<6.0', 'djangorestframework~=3.9', 'python-dateutil~=2.8.1', 'pytz>=2019.3', + 'requests~=2.27.1', 'webpack-manifest~=2.1', ], - python_requires='>=3.6, <3.8', + python_requires='>=3.7, <3.11', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -77,8 +78,10 @@ def run(self): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries :: Python Modules', ], diff --git a/spec/Suite.tsx b/spec/Suite.tsx index 284164d0a..5e1c6de03 100644 --- a/spec/Suite.tsx +++ b/spec/Suite.tsx @@ -1,12 +1,14 @@ import * as React from 'react'; import * as Enzyme from 'enzyme'; import { mount, shallow } from 'enzyme'; -import { AnyAction, applyMiddleware, createStore, Store } from 'redux'; import Adapter from 'enzyme-adapter-react-16'; -import { combinedReducer, StoreState } from '../bundles/tracker/Store'; -import './matchers'; import { Provider } from 'react-redux'; +import { AnyAction, applyMiddleware, createStore, Store } from 'redux'; + import { setAPIRoot } from '../bundles/tracker/Endpoints'; +import { combinedReducer, StoreState } from '../bundles/tracker/Store'; + +import './matchers'; let componentFakes: any[] = []; let oldCreateElement: typeof React.createElement; diff --git a/tests/randgen.py b/tests/randgen.py index 89fe0a4b6..910743138 100644 --- a/tests/randgen.py +++ b/tests/randgen.py @@ -354,10 +354,10 @@ def generate_donation( ): donation = Donation() donation.amount = random_amount(rand, min_amount=min_amount, max_amount=max_amount) - if event: - donation.event = event - else: - donation.event = pick_random_instance(rand, Event) + if not event: + event = pick_random_instance(rand, Event) + assert event, 'No event provided and none exist' + donation.event = event if domain: donation.domain = domain else: diff --git a/tests/requirements.txt b/tests/requirements.txt index 3b5ab7dc8..c829120e5 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,19 +1,20 @@ # base -celery==5.1.1 +celery==5.2.7 channels==3.0.4 -Django==2.2.24 +# django explicitly not listed here because azure installs a particular series immediately after django-ajax-selects==1.9.1 -django-ical==1.8.0 -django-paypal==1.0.0 -django-mptt==0.12.0 -django-post-office==3.5.3 -django-timezone-field==4.2.1 -djangorestframework==3.12.4 -pre-commit==2.13.0 +django-ical==1.8.3 +django-paypal==1.1.2 +django-mptt==0.13.4 +django-post-office==3.6.0 +django-timezone-field==5.0 +djangorestframework==3.13.1 +pre-commit==2.19.0 python-dateutil==2.8.2 -pytz==2021.1 +pytz==2022.1 webpack-manifest==2.1.1 # only for testing -responses~=0.13.3 +responses~=0.21.0 +selenium==4.2.0 tblib==1.7.0 -unittest-xml-reporting==3.0.4 +unittest-xml-reporting==3.2.0 diff --git a/tests/test_admin.py b/tests/test_admin.py index 3b6e73bd3..ee219a7a0 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -7,6 +7,7 @@ from tracker import models from . import randgen +from .util import TrackerSeleniumTestCase User = get_user_model() @@ -31,56 +32,88 @@ def test_get_loads(self): self.assertContains(response, 'Select which donor to use as the template') -class ProcessDonationsTest(TestCase): +class ProcessDonationsBrowserTest(TrackerSeleniumTestCase): def setUp(self): self.rand = random.Random(None) self.superuser = User.objects.create_superuser( 'superuser', 'super@example.com', 'password', ) self.processor = User.objects.create(username='processor', is_staff=True) + self.processor.set_password('password') self.processor.user_permissions.add( Permission.objects.get(name='Can change donor'), Permission.objects.get(name='Can change donation'), + Permission.objects.get(name='Can view all comments'), ) + self.processor.save() self.head_processor = User.objects.create( username='head_processor', is_staff=True ) + self.head_processor.set_password('password') self.head_processor.user_permissions.add( Permission.objects.get(name='Can change donor'), Permission.objects.get(name='Can change donation'), Permission.objects.get(name='Can send donations to the reader'), + Permission.objects.get(name='Can view all comments'), ) + self.head_processor.save() self.event = randgen.build_random_event(self.rand) self.session = self.client.session self.session.save() + self.donor = randgen.generate_donor(self.rand) + self.donor.save() + self.donation = randgen.generate_donation( + self.rand, commentstate='PENDING', readstate='PENDING' + ) + self.donation.save() def test_one_step_screening(self): - self.client.force_login(self.processor) - response = self.client.get( - reverse('admin:process_donations', args=(self.event.short,)) + self.tracker_login(self.processor.username) + self.webdriver.get( + f'{self.live_server_url}{reverse("admin:tracker_ui")}/process_donations/{str(self.event.id)}' ) - self.assertEqual(response.context['user_can_approve'], True) - self.assertEqual(response.status_code, 200) - - def test_two_step_screening_non_head(self): - self.event.use_one_step_screening = False - self.event.save() - self.client.force_login(self.processor) - response = self.client.get( - reverse('admin:process_donations', args=(self.event.short,)) + self.wait_for_spinner() + row = self.webdriver.find_element_by_css_selector( + f'tr[data-test-pk="{self.donation.pk}"]' ) - self.assertEqual(response.context['user_can_approve'], False) - self.assertEqual(response.status_code, 200) + row.find_element_by_css_selector('button[data-test-id="send"]').click() + self.wait_for_spinner() + self.donation.refresh_from_db() + self.assertEqual(self.donation.readstate, 'READY') - def test_two_step_screening_with_head(self): + def test_two_step_screening(self): self.event.use_one_step_screening = False self.event.save() - self.client.force_login(self.head_processor) - response = self.client.get( - reverse('admin:process_donations', args=(self.event.short,)) - ) - self.assertEqual(response.context['user_can_approve'], True) - self.assertEqual(response.status_code, 200) + self.tracker_login(self.processor.username) + self.webdriver.get( + f'{self.live_server_url}{reverse("admin:tracker_ui")}/process_donations/{str(self.event.id)}' + ) + self.wait_for_spinner() + row = self.webdriver.find_element_by_css_selector( + f'tr[data-test-pk="{self.donation.pk}"]' + ) + row.find_element_by_css_selector('button[data-test-id="send"]').click() + self.wait_for_spinner() + self.donation.refresh_from_db() + self.assertEqual(self.donation.readstate, 'FLAGGED') + self.tracker_logout() + self.tracker_login(self.head_processor.username) + self.webdriver.get( + f'{self.live_server_url}{reverse("admin:tracker_ui")}/process_donations/{str(self.event.id)}' + ) + self.wait_for_spinner() + self.select_option('[data-test-id="processing-mode"]', 'confirm') + self.webdriver.find_element_by_css_selector( + 'button[data-test-id="refresh"' + ).click() + self.wait_for_spinner() + row = self.webdriver.find_element_by_css_selector( + f'tr[data-test-pk="{self.donation.pk}"]' + ) + row.find_element_by_css_selector('button[data-test-id="send"]').click() + self.wait_for_spinner() + self.donation.refresh_from_db() + self.assertEqual(self.donation.readstate, 'READY') class TestAdminViews(TestCase): @@ -94,26 +127,6 @@ def setUp(self): self.session = self.client.session self.session.save() - def test_read_donations(self): - self.client.force_login(self.superuser) - response = self.client.get(reverse('admin:read_donations')) - self.assertEqual(response.status_code, 200) - - response = self.client.get( - reverse('admin:read_donations', args=(self.event.short,)) - ) - self.assertEqual(response.status_code, 200) - - def test_process_donations(self): - self.client.force_login(self.superuser) - response = self.client.get(reverse('admin:process_donations')) - self.assertEqual(response.status_code, 200) - - response = self.client.get( - reverse('admin:process_donations', args=(self.event.short,)) - ) - self.assertEqual(response.status_code, 200) - def test_merge_bids(self): self.client.force_login(self.superuser) randgen.generate_runs(self.rand, self.event, 5) @@ -125,16 +138,6 @@ def test_merge_bids(self): self.assertEqual(response.status_code, 200) self.assertContains(response, 'Select which bid to use as the template') - def test_process_pending_bids(self): - self.client.force_login(self.superuser) - response = self.client.get(reverse('admin:process_pending_bids')) - self.assertEqual(response.status_code, 200) - - response = self.client.get( - reverse('admin:process_pending_bids', args=(self.event.short,)) - ) - self.assertEqual(response.status_code, 200) - def test_automail_prize_contributors(self): self.client.force_login(self.superuser) response = self.client.get(reverse('admin:automail_prize_contributors')) diff --git a/tests/test_api.py b/tests/test_api.py index 368019427..194593b23 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -979,6 +979,7 @@ def add_event_fields(fields, event, prefix): goal=bid.goal, state=bid.state, istarget=bid.istarget, + pinned=bid.pinned, revealedtime=format_time(bid.revealedtime), allowuseroptions=bid.allowuseroptions, biddependency=bid.biddependency_id, diff --git a/tests/test_bid.py b/tests/test_bid.py index 6ecc186f1..7f7e62c9c 100644 --- a/tests/test_bid.py +++ b/tests/test_bid.py @@ -30,6 +30,9 @@ def setUp(self): self.donation = models.Donation.objects.create( donor=self.donor, event=self.event, amount=5, transactionstate='COMPLETED' ) + self.donation2 = models.Donation.objects.create( + donor=self.donor, event=self.event, amount=10, transactionstate='COMPLETED' + ) self.opened_parent_bid = models.Bid.objects.create( name='Opened Parent Test', speedrun=self.run, @@ -81,6 +84,17 @@ def setUp(self): state='PENDING', ) self.pending_bid.save() + self.challenge = models.Bid.objects.create( + name='Challenge', + istarget=True, + state='OPENED', + pinned=True, + goal=15, + speedrun=self.run, + ) + self.challenge_donation = models.DonationBid.objects.create( + donation=self.donation2, bid=self.challenge, amount=self.donation2.amount, + ) class TestBid(TestBidBase): @@ -152,42 +166,67 @@ def test_pending_bid(self): self.opened_parent_bid.total, 0, msg='parent bid total is wrong' ) + def test_autoclose(self): + self.challenge.refresh_from_db() + self.assertEqual(self.challenge.state, 'OPENED') + self.assertTrue(self.challenge.pinned) + models.DonationBid.objects.create( + donation=self.donation, bid=self.challenge, amount=self.donation.amount + ) + self.challenge.refresh_from_db() + self.assertEqual(self.challenge.state, 'CLOSED') + self.assertFalse(self.challenge.pinned) + def test_state_propagation(self): for state in ['CLOSED', 'HIDDEN', 'OPENED']: - self.opened_parent_bid.state = state - self.opened_parent_bid.save() - self.opened_bid.refresh_from_db() - self.assertEqual( - self.opened_bid.state, - state, - msg=f'Child state `{state}` did not propagate from parent during parent save', - ) - for bid in [self.pending_bid, self.denied_bid]: - old_state = bid.state - bid.refresh_from_db() + with self.subTest(state=state): + self.opened_parent_bid.state = state + self.opened_parent_bid.save() + self.opened_bid.refresh_from_db() self.assertEqual( - bid.state, - old_state, - msg=f'Child state `{old_state}` should not have changed during parent save', + self.opened_bid.state, + state, + msg=f'Child state `{state}` did not propagate from parent during parent save', ) + for bid in [self.pending_bid, self.denied_bid]: + with self.subTest(child_state=bid.state): + old_state = bid.state + bid.refresh_from_db() + self.assertEqual( + bid.state, + old_state, + msg=f'Child state `{old_state}` should not have changed during parent save', + ) for state in ['CLOSED', 'HIDDEN']: - self.opened_bid.state = state - self.opened_bid.save() - self.opened_bid.refresh_from_db() - self.assertEqual( - self.opened_bid.state, - 'OPENED', - msg=f'Child state `{state}` did not propagate from parent during child save', - ) + with self.subTest(child_state=state): + self.opened_bid.state = state + self.opened_bid.save() + self.opened_bid.refresh_from_db() + self.assertEqual( + self.opened_bid.state, + 'OPENED', + msg=f'Child state `{state}` did not propagate from parent during child save', + ) for state in ['PENDING', 'DENIED']: - self.opened_bid.state = state - self.opened_bid.save() - self.opened_bid.refresh_from_db() - self.assertEqual( - self.opened_bid.state, - state, - msg=f'Child state `{state}` should not have propagated from parent during child save', - ) + with self.subTest(child_state=state): + self.opened_bid.state = state + self.opened_bid.save() + self.opened_bid.refresh_from_db() + self.assertEqual( + self.opened_bid.state, + state, + msg=f'Child state `{state}` should not have propagated from parent during child save', + ) + + def test_pin_propagation(self): + self.opened_parent_bid.pinned = True + self.opened_parent_bid.save() + self.opened_bid.refresh_from_db() + self.assertTrue(self.opened_bid.pinned, msg='Child pin flag did not propagate') + self.opened_parent_bid.pinned = False + self.opened_parent_bid.save() + self.opened_bid.refresh_from_db() + self.assertFalse(self.opened_bid.pinned, msg='Child pin flag did not propagate') def test_bid_option_max_length_require(self): # A bid cannot set option_max_length if allowuseroptions is not set @@ -283,7 +322,15 @@ def test_bid_event_list(self): self.assertContains(resp, reverse('tracker:bidindex', args=(self.event.short,))) def test_bid_list(self): + models.DonationBid.objects.create( + donation=self.donation, bid=self.opened_bid, amount=self.donation.amount + ) resp = self.client.get(reverse('tracker:bidindex', args=(self.event.short,))) + self.assertContains( + resp, f'Total: ${(self.donation.amount + self.donation2.amount):.2f}' + ) + self.assertContains(resp, f'Choice Total: ${self.donation.amount:.2f}') + self.assertContains(resp, f'Challenge Total: ${self.donation2.amount:.2f}') self.assertContains(resp, self.opened_parent_bid.name) self.assertContains(resp, self.opened_parent_bid.get_absolute_url()) self.assertContains(resp, self.opened_bid.name) diff --git a/tests/test_donation.py b/tests/test_donation.py index c376a794f..51b7e5680 100644 --- a/tests/test_donation.py +++ b/tests/test_donation.py @@ -4,7 +4,7 @@ from decimal import Decimal from unittest.mock import patch -from django.contrib.admin import ACTION_CHECKBOX_NAME +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import User from django.test import TestCase, override_settings from django.urls import reverse diff --git a/tests/test_event.py b/tests/test_event.py index 5e0813773..d10e8cf49 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -7,7 +7,7 @@ import pytz from django.conf import settings from django.contrib.auth.models import User, Group, Permission -from django.test import TestCase, override_settings +from django.test import TestCase, override_settings, TransactionTestCase from django.urls import reverse from tracker import models @@ -38,7 +38,7 @@ def test_update_first_run_if_event_time_changes(self): self.assertEqual(self.run.starttime, self.event.datetime) -class TestEventViews(TestCase): +class TestEventViews(TransactionTestCase): def setUp(self): self.event = models.Event.objects.create( targetamount=1, datetime=today_noon, short='short', name='Short' @@ -46,10 +46,18 @@ def setUp(self): @override_settings(TRACKER_LOGO='example-logo.png') def test_main_index(self): - # TODO: make this more than just a smoke test + models.Donation.objects.create( + event=self.event, amount=5, transactionstate='COMPLETED' + ) + models.Donation.objects.create( + event=self.event, amount=10, transactionstate='COMPLETED' + ) response = self.client.get(reverse('tracker:index_all')) self.assertContains(response, 'All Events') self.assertContains(response, 'example-logo.png') + self.assertContains(response, '$15.00 (2)', 1) + self.assertContains(response, '$10.00', 1) + self.assertContains(response, '$7.50', 2) def test_json_with_no_donations(self): response = self.client.get( diff --git a/tests/test_prize.py b/tests/test_prize.py index c2f111f36..c233cbb23 100644 --- a/tests/test_prize.py +++ b/tests/test_prize.py @@ -7,7 +7,7 @@ import pytz from dateutil.parser import parse as parse_date from django.conf import settings -from django.contrib.admin import ACTION_CHECKBOX_NAME +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import User from django.core.exceptions import ( ValidationError, diff --git a/tests/test_search_filters.py b/tests/test_search_filters.py index 7740f484b..8ed7ec457 100644 --- a/tests/test_search_filters.py +++ b/tests/test_search_filters.py @@ -2,6 +2,7 @@ import datetime import random +import pytz from django.contrib.auth.models import User, Permission from django.core.exceptions import PermissionDenied from django.db.models import Q @@ -45,6 +46,9 @@ def setUp(self): ) self.opened_bids += denied_bids[0] self.denied_bids = denied_bids[1] + self.pinned_bid = opened_bids[0][-1] + self.pinned_bid.pinned = True + self.pinned_bid.save() self.accepted_prizes = randgen.generate_prizes(self.rand, self.event, 5) self.pending_prizes = randgen.generate_prizes( self.rand, self.event, 5, state='PENDING' @@ -185,6 +189,51 @@ def test_closed_feed(self): expected = self.query.filter(state='CLOSED') self.assertSetEqual(set(actual), set(expected)) + # TODO: these need more detailed tests + def test_current_feed(self): + actual = apply_feed_filter( + self.query, + 'bid', + 'current', + params=dict(time=self.event.datetime, min_runs=0, max_runs=5), + ) + expected = self.query.filter( + Q( + speedrun__in=( + r.pk + for r in self.event.speedrun_set.filter( + endtime__lte=self.event.datetime.astimezone(pytz.utc) + + datetime.timedelta(hours=6) + )[:5] + ) + ) + | Q(pinned=True), + state='OPENED', + ) + self.assertSetEqual(set(actual), set(expected)) + + def test_current_plus_feed(self): + actual = apply_feed_filter( + self.query, + 'bid', + 'current_plus', + params=dict(time=self.event.datetime, min_runs=0, max_runs=5), + ) + expected = self.query.filter( + Q( + speedrun__in=( + r.pk + for r in self.event.speedrun_set.filter( + endtime__lte=self.event.datetime.astimezone(pytz.utc) + + datetime.timedelta(hours=6) + )[:5] + ) + ) + | Q(pinned=True), + state__in=['OPENED', 'CLOSED'], + ) + self.assertSetEqual(set(actual), set(expected)) + def test_all_feed_without_permission(self): with self.assertRaises(PermissionDenied): apply_feed_filter(self.query, 'bid', 'all') diff --git a/tests/test_settings.py b/tests/test_settings.py index bd6ee604d..e9cd06d0b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -20,7 +20,11 @@ 'mptt', ] DATABASES = { - 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'testdb.sqlite',}, + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'testdb.sqlite', + 'OPTIONS': {'timeout': 5}, + }, } STATIC_URL = '/static/' STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static') @@ -39,6 +43,7 @@ 'django.template.context_processors.debug', 'django.template.context_processors.i18n', 'django.template.context_processors.media', + 'django.template.context_processors.request', 'django.template.context_processors.static', 'django.template.context_processors.tz', 'django.contrib.messages.context_processors.messages', @@ -61,3 +66,12 @@ ASGI_APPLICATION = 'tests.routing.application' CHANNEL_LAYERS = {'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}} SWEEPSTAKES_URL = 'https://example.com/' +TEST_OUTPUT_DIR = 'test-results' +# uncomment this for some additional logging during testing +# LOGGING = { +# 'version': 1, +# 'disable_existing_loggers': False, +# 'handlers': {'console': {'level': 'DEBUG', 'class': 'logging.StreamHandler',},}, +# 'loggers': {'django': {'handlers': ['console'],},}, +# 'root': {'level': 'INFO'}, +# } diff --git a/tests/test_util.py b/tests/test_util.py index 668372a04..34cf20695 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -49,6 +49,8 @@ def testInvalidReplaceLen(self): with self.assertRaises(Exception): util.random_num_replace(original, replaceLen, max_length=maxLen) + +class TestUtil(TestCase): def test_median(self): self.assertEqual(util.median(models.Donation.objects.all(), 'amount'), 0) event = models.Event.objects.create(datetime=today_noon) @@ -57,3 +59,9 @@ def test_median(self): self.assertEqual(util.median(models.Donation.objects.all(), 'amount'), 5) models.Donation.objects.create(event=event, amount=21) self.assertEqual(util.median(models.Donation.objects.all(), 'amount'), 6.5) + + def test_flatten(self): + self.assertSequenceEqual( + list(util.flatten(['string', [1, 2, 'also_string', [3, 4, 5]]])), + ['string', 1, 2, 'also_string', 3, 4, 5], + ) diff --git a/tests/urls.py b/tests/urls.py index 78c52171b..3ce967b12 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,11 +1,21 @@ import ajax_select.urls from django.contrib import admin +from django.http import HttpResponse from django.urls import include, path +from django.contrib.staticfiles.urls import staticfiles_urlpatterns import tracker.urls + +def empty(request): + return HttpResponse('') + + urlpatterns = [ path('tracker/', include(tracker.urls)), path('admin/lookups/', include(ajax_select.urls)), path('admin/', admin.site.urls), + path('favicon.ico', empty), ] + +urlpatterns += staticfiles_urlpatterns() diff --git a/tests/util.py b/tests/util.py index 45fc43106..9e707cf39 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,14 +1,24 @@ import datetime import json import random +import functools +import unittest +import time import pytz from django.conf import settings from django.contrib.auth.models import AnonymousUser, User, Permission +from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core.serializers.json import DjangoJSONEncoder from django.db import connection from django.db.migrations.executor import MigrationExecutor -from django.test import TransactionTestCase, RequestFactory +from django.test import TransactionTestCase, RequestFactory, override_settings +from django.urls import reverse +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import Select, WebDriverWait +from selenium.webdriver.support import expected_conditions as EC from tracker import models @@ -217,3 +227,87 @@ def setUp(self): ) self.super_user = User.objects.create(username='super', is_superuser=True) self.maxDiff = None + + +def _tag_error(func): + """Decorates a unittest test function to add failure information to the TestCase.""" + + @functools.wraps(func) + def decorator(self, *args, **kwargs): + """Add failure information to `self` when `func` raises an exception.""" + self.test_failed = False + try: + func(self, *args, **kwargs) + except unittest.SkipTest: + raise + except Exception: # pylint: disable=broad-except + self.test_failed = True + raise # re-raise the error with the original traceback. + + return decorator + + +class _TestFailedMeta(type): + """Metaclass to decorate test methods to append error information to the TestCase instance.""" + + def __new__(mcs, name, bases, dct): + for name, prop in dct.items(): + # assume that TestLoader.testMethodPrefix hasn't been messed with -- otherwise, we're hosed. + if name.startswith('test') and callable(prop): + dct[name] = _tag_error(prop) + + return super().__new__(mcs, name, bases, dct) + + +@override_settings(DEBUG=True) +class TrackerSeleniumTestCase(StaticLiveServerTestCase, metaclass=_TestFailedMeta): + @classmethod + def setUpClass(cls): + super().setUpClass() + options = Options() + options.headless = True + cls.webdriver = webdriver.Firefox(options=options) + cls.webdriver.implicitly_wait(5) + + @classmethod + def tearDownClass(cls): + cls.webdriver.quit() + super().tearDownClass() + + def tearDown(self): + super().tearDown() + if self.test_failed: + self.webdriver.get_screenshot_as_file( + f'./test-results/TEST-{self.id()}.{int(time.time())}.png' + ) + raise Exception( + f'data:image/png;base64,{self.webdriver.get_screenshot_as_base64()}' + ) + + def tracker_login(self, username, password='password'): + self.webdriver.get(self.live_server_url + reverse('admin:login')) + self.webdriver.find_element_by_name('username').send_keys(username) + self.webdriver.find_element_by_name('password').send_keys(password) + self.webdriver.find_element_by_css_selector('form input[type=submit]').click() + self.webdriver.find_element_by_css_selector( + '.app-tracker' + ) # admin page has loaded + + def tracker_logout(self): + self.webdriver.get(self.live_server_url + reverse('admin:logout')) + self.assertEqual( + self.webdriver.find_element_by_css_selector('#content h1').text, + 'Logged out', + ) + + def select_option(self, selector, value): + Select(self.webdriver.find_element_by_css_selector(selector)).select_by_value( + value + ) + + def wait_for_spinner(self): + WebDriverWait(self.webdriver, 5).until_not( + EC.presence_of_element_located( + (By.CSS_SELECTOR, '[data-test-id="spinner"]') + ) + ) diff --git a/tracker/__init__.py b/tracker/__init__.py index e69de29bb..ab1f88c10 100644 --- a/tracker/__init__.py +++ b/tracker/__init__.py @@ -0,0 +1 @@ +default_app_config = 'tracker.apps.TrackerAppConfig' diff --git a/tracker/admin/bid.py b/tracker/admin/bid.py index e3968ec6e..dae8a31b8 100644 --- a/tracker/admin/bid.py +++ b/tracker/admin/bid.py @@ -1,5 +1,3 @@ -import json - from django.contrib import messages from django.contrib.admin import register from django.contrib.auth.decorators import permission_required @@ -12,10 +10,7 @@ from .filters import BidListFilter, BidParentFilter from .forms import DonationBidForm, BidForm from .inlines import BidOptionInline, BidDependentsInline -from .util import ( - CustomModelAdmin, - api_urls, -) +from .util import CustomModelAdmin @register(models.Bid) @@ -59,6 +54,7 @@ class BidAdmin(CustomModelAdmin): 'shortdescription', 'goal', 'istarget', + 'pinned', 'allowuseroptions', 'option_max_length', 'revealedtime', @@ -206,28 +202,6 @@ def merge_bids_view(request, *args, **kwargs): form = forms.MergeObjectsForm(model=models.Bid, objects=objects) return render(request, 'admin/tracker/merge_bids.html', {'form': form}) - @staticmethod - @permission_required('tracker.change_bid') - def process_pending_bids(request, event=None): - event = viewutil.get_event(event) - - if not event.id: - return render( - request, - 'tracker/eventlist.html', - { - 'events': models.Event.objects.all(), - 'pattern': 'admin:process_pending_bids', - 'subheading': 'Process Pending Bids', - }, - ) - - return render( - request, - 'admin/tracker/process_pending_bids.html', - {'currentEvent': event, 'apiUrls': mark_safe(json.dumps(api_urls())),}, - ) - def get_urls(self): return super(BidAdmin, self).get_urls() + [ path( @@ -235,16 +209,6 @@ def get_urls(self): self.admin_site.admin_view(self.merge_bids_view), name='merge_bids', ), - path( - 'process_pending_bids', - self.admin_site.admin_view(self.process_pending_bids), - name='process_pending_bids', - ), - path( - 'process_pending_bids/', - self.admin_site.admin_view(self.process_pending_bids), - name='process_pending_bids', - ), ] def get_actions(self, request): diff --git a/tracker/admin/donation.py b/tracker/admin/donation.py index 0868f7b37..d4629d239 100644 --- a/tracker/admin/donation.py +++ b/tracker/admin/donation.py @@ -1,4 +1,3 @@ -import json from datetime import datetime, timedelta from django.conf import settings @@ -7,9 +6,8 @@ from django.contrib.auth.decorators import permission_required from django.http import HttpResponseRedirect from django.shortcuts import render -from django.urls import reverse, path +from django.urls import reverse from django.utils.html import format_html -from django.utils.safestring import mark_safe from tracker import search_filters, forms, logutil, viewutil, models from .filters import DonationListFilter @@ -18,7 +16,6 @@ from .util import ( CustomModelAdmin, mass_assign_action, - api_urls, ) @@ -207,87 +204,6 @@ def get_queryset(self, request): params['feed'] = 'all' return search_filters.run_model_query('donation', params, user=request.user) - def get_urls(self): - return super(DonationAdmin, self).get_urls() + [ - path( - 'process_donations', - self.admin_site.admin_view(self.process_donations), - name='process_donations', - ), - path( - 'process_donations/', - self.admin_site.admin_view(self.process_donations), - name='process_donations', - ), - path( - 'read_donations', - self.admin_site.admin_view(self.read_donations), - name='read_donations', - ), - path( - 'read_donations/', - self.admin_site.admin_view(self.read_donations), - name='read_donations', - ), - ] - - @staticmethod - @permission_required(('tracker.change_donation',)) - def process_donations(request, event=None): - event = viewutil.get_event(event) - - if not event.id: - return render( - request, - 'tracker/eventlist.html', - { - 'events': models.Event.objects.all(), - 'pattern': 'admin:process_donations', - 'subheading': 'Process Pending Bids', - }, - ) - - user_can_approve = ( - event and event.use_one_step_screening - ) or request.user.has_perm('tracker.send_to_reader') - user_can_edit_donors = request.user.has_perm('tracker.change_donor') - return render( - request, - 'admin/tracker/process_donations.html', - { - 'user_can_approve': user_can_approve, - 'user_can_edit_donors': user_can_edit_donors, - 'currentEvent': event, - 'apiUrls': mark_safe(json.dumps(api_urls())), - }, - ) - - @staticmethod - @permission_required(('tracker.change_donation',)) - def read_donations(request, event=None): - event = viewutil.get_event(event) - - if not event.id: - return render( - request, - 'tracker/eventlist.html', - { - 'events': models.Event.objects.all(), - 'pattern': 'admin:process_pending_bids', - 'subheading': 'Process Pending Bids', - }, - ) - user_can_edit_donors = request.user.has_perm('tracker.change_donor') - return render( - request, - 'admin/tracker/read_donations.html', - { - 'user_can_edit_donors': user_can_edit_donors, - 'currentEvent': event, - 'apiUrls': mark_safe(json.dumps(api_urls())), - }, - ) - actions = [ set_readstate_ready, set_readstate_ignored, diff --git a/tracker/analytics/README.md b/tracker/analytics/README.md new file mode 100644 index 000000000..e624b06a4 --- /dev/null +++ b/tracker/analytics/README.md @@ -0,0 +1,41 @@ +# Tracker Analytics + +The tracker code is instrumented to automatically create and send out analytics events for a variety of actions. These events allow you to construct real-time logs and dashboards to track information over time that is otherwise not available from the tracker's own models, such as "how much had an event raised at this point in time" or "what states did a donation go through". + +However, the tracker does _not_ provide these analytics tools or storage for events internally. Instead, events are gathered throughout the request lifecycle and then emitted over HTTP to an analytics server. + +We do not currently have an open-sourced server for reference, but the format that is emitted is relatively simple JSON: + +```json +[ + { "event_name": "name_of_event", "properties": { ...event_properties } }, + { "event_name": "another_event", "properties": { ...another_event_properties } } +] +``` + +This information can then be stored and processed in whatever way works best for you. + +# Configuration + +There are a number of settings parameters (set in your main `settings.py`) to control analytics behavior: + +**`TRACKER_ANALYTICS_INGEST_HOST`** - The URL for sending analytics events to. Example: 'http://localhost:5000' +**`TRACKER_ANALYTICS_NO_EMIT`** - When `True`, track events like normal, but don't actually emit them to the ingest host. +**`TRACKER_ANALYTICS_TEST_MODE`** - When `True`, Use the `test_path` path of the analytics host to send events. This is useful for end-to-end validation. +**`TRACKER_ANALYTICS_ACCESS_KEY`** - When set, this key will be sent as an `x-analytics-key` header to provide authentication for all analytics requests. + +# Development + +The following sections are relevant for making changes to the tracker and instrumentation itself. If you are just using a tracker instance in production, you do not need to do any of these things. + +### Adding a new event + +For now, events are very loosely defined, and validation of properties and event types should be done on the ingest side (i.e., where events are sent over HTTP). + +To add a new event for tracking, add an entry with the name of the event to `AnalyticsEventTypes` in `./events.py`. Then wherever it is relevant in the tracker code, add a call to `analytics.track` with the event's information. + +```python +from tracker.analytics import analytics, AnalyticsEventTypes + +analytics.track(AnalyticsEventTypes.EVENT_NAME, { 'some-data': 'some-value' }) +``` diff --git a/tracker/analytics/__init__.py b/tracker/analytics/__init__.py new file mode 100644 index 000000000..496de44c2 --- /dev/null +++ b/tracker/analytics/__init__.py @@ -0,0 +1,24 @@ +from contextlib import contextmanager + +from django.conf import settings + +from tracker.analytics.client import AnalyticsClient +from tracker.analytics.events import AnalyticsEventTypes # noqa: F401 + + +analytics = AnalyticsClient( + AnalyticsClient.Config( + access_key=getattr(settings, 'TRACKER_ANALYTICS_ACCESS_KEY', None), + ingest_host=getattr(settings, 'TRACKER_ANALYTICS_INGEST_HOST', ''), + test_mode=getattr(settings, 'TRACKER_ANALYTICS_TEST_MODE', False), + no_emit=getattr(settings, 'TRACKER_ANALYTICS_NO_EMIT', True), + ) +) + + +@contextmanager +def analytics_context(client: AnalyticsClient): + try: + yield client + finally: + client.flush() diff --git a/tracker/analytics/client.py b/tracker/analytics/client.py new file mode 100644 index 000000000..180d65696 --- /dev/null +++ b/tracker/analytics/client.py @@ -0,0 +1,173 @@ +# Adapted from https://github.com/GamesDoneQuick/analytics-packages/blob/0.1.0/analytics.py +# until that package stabilizes +import atexit +from datetime import datetime, date, timedelta +from decimal import Decimal +import json +import logging +from threading import Thread +import time +import typing as t +from queue import Queue, Empty + +import requests + +from .events import AnalyticsEventTypes + +logger = logging.getLogger('analytics') + + +class AnalyticsClient: + class Config: + ingest_url: str + + def __init__( + self, + *, + access_key: str = None, + ingest_host: str = 'http://localhost:5000', + # Maximum number of buffered events before `flush` will be called + # automatically to avoid an overflow. + max_buffer_size: int = 100, + # Maximum amount of time in seconds to wait before automatically + # flushing the event buffer + max_wait_time: float = 2.0, + # Don't emit any events to the ingest host. Tracks will still work + # and be buffered, but no HTTP calls will be made. + no_emit: bool = False, + # Use the `test_path` endpoint instead of actually ingesting events. + test_mode: bool = False, + path: str = '/track', + test_path: str = '/test', + ): + self.access_key = access_key + self.ingest_host = ingest_host + self.max_buffer_size = max_buffer_size + self.max_wait_time = max_wait_time + self.no_emit = no_emit + self.path = path + self.test_path = test_path + self.test_mode = test_mode + + resolved_path = test_path if test_mode else path + self.ingest_url = f'{ingest_host}{resolved_path}' + + def __init__(self, config: Config): + self.config = config + self.queue = Queue(maxsize=0) + self.consumer = Consumer(self.config, self.queue) + atexit.register(self.join) + # Only start the Consumer if we are going to be emitting events. + if not self.config.no_emit: + self.consumer.start() + + def set_access_key(self, access_key: t.Union[str, None]): + self.config.access_key = access_key + + def track(self, event_name: AnalyticsEventTypes, data: t.Dict[str, t.Any]): + """Track a single analytics event.""" + event_content = {'event_name': event_name.value, 'properties': data} + logger.debug(f'[Analytics Client] Tracked event: {event_content}') + if not self.config.no_emit: + self.queue.put(event_content) + + def track_generic(self, event_name: str, data: t.Dict[str, t.Any]): + """ + Track a single analytics event, where the event_name may not be + known, primarily used for proxying analytics through this service. + """ + event_content = {'event_name': event_name, 'properties': data} + logger.debug(f'[Analytics Client] Tracked event: {event_content}') + if not self.config.no_emit: + self.queue.put(event_content) + + def join(self): + """Allow the consumer thread to gracefully finish before returning.""" + self.consumer.pause() + try: + self.consumer.join() + # This can raise if the consumer thread was never started + except RuntimeError: + pass + + def flush(self): + """ + Force the current queue of events to be uploaded to the ingest host + immediately. This shouldn't really need to be called most of the time. + """ + size = self.queue.qsize() + self.queue.join() + logger.debug(f'[Analytics Client] Forcefully flushed {size} events') + + +class Consumer(Thread): + logger = logging.getLogger('analytics') + + def __init__(self, config: AnalyticsClient.Config, queue: Queue): + Thread.__init__(self) + self.config = config + self.queue = queue + self.daemon = True + self.running = True + + def pause(self): + self.running = False + + def run(self): + while self.running: + batch = self._get_batch() + + try: + self.upload(batch) + except Exception as e: + logger.error(f'[Analytics Consumer] Failed to process events: {e}') + finally: + for _ in batch: + self.queue.task_done() + + def _get_batch(self): + start = time.monotonic() + events = [] + + while len(events) < self.config.max_buffer_size: + elapsed = time.monotonic() - start + if elapsed > self.config.max_wait_time: + break + + remaining = self.config.max_wait_time - elapsed + try: + event = self.queue.get(block=True, timeout=remaining) + events.append(event) + except Empty: + break + + return events + + def upload(self, events): + """Send all buffered events to the ingest server.""" + if len(events) == 0: + return + + headers = {'Content-Type': 'application/json'} + if self.config.access_key is not None: + headers['x-analytics-key'] = self.config.access_key + + return requests.post( + self.config.ingest_url, + data=json.dumps(events, cls=AnalyticsJSONEncoder), + headers=headers, + ) + + +class AnalyticsJSONEncoder(json.JSONEncoder): + def default(self, obj): + # Datetimes are formatted to ISO format + if isinstance(obj, (datetime, date)): + return obj.isoformat() + # Time deltas are formatted to milliseconds with decimal precision + if isinstance(obj, timedelta): + return obj / timedelta(milliseconds=1) + if isinstance(obj, Decimal): + return str(obj) + + return json.JSONEncoder.default(self, obj) diff --git a/tracker/analytics/events.py b/tracker/analytics/events.py new file mode 100644 index 000000000..87c2a4028 --- /dev/null +++ b/tracker/analytics/events.py @@ -0,0 +1,12 @@ +import enum + + +class AnalyticsEventTypes(enum.Enum): + BID_APPLIED = 'bid_applied' + INCENTIVE_OPENED = 'incentive_opened' + INCENTIVE_MET = 'incentive_met' + DONATION_RECEIVED = 'donation_received' + DONATION_PENDING = 'donation_pending' + DONATION_COMPLETED = 'donation_completed' + DONATION_CANCELLED = 'donation_cancelled' + REQUEST_SERVED = 'request_served' diff --git a/tracker/analytics/middleware.py b/tracker/analytics/middleware.py new file mode 100644 index 000000000..2baaa3206 --- /dev/null +++ b/tracker/analytics/middleware.py @@ -0,0 +1,47 @@ +import asyncio +from datetime import datetime + +from tracker.analytics import analytics, AnalyticsEventTypes + + +def AnalyticsMiddleware(get_response): + def track_request(request, started, finished): + analytics.track( + AnalyticsEventTypes.REQUEST_SERVED, + { + 'timestamp': started, + 'duration': finished - started, + 'path': request.path, + 'method': request.method, + 'content_type': request.content_type, + }, + ) + + if asyncio.iscoroutinefunction(get_response): + + async def async_middleware(request): + started = datetime.utcnow() + response = await get_response(request) + finished = datetime.utcnow() + track_request(request, started, finished) + return response + + return async_middleware + + else: + + def sync_middleware(request): + started = datetime.utcnow() + response = get_response(request) + finished = datetime.utcnow() + track_request(request, started, finished) + return response + + return sync_middleware + + +# TODO: The `sync_and_async_middleware` decorator is not available on all versions +# of Django that we currently support. This has the same effect, but should change +# to use the decorator once Django 2.2 support is dropped. +AnalyticsMiddleware.async_capable = True +AnalyticsMiddleware.sync_capable = True diff --git a/tracker/apps.py b/tracker/apps.py new file mode 100644 index 000000000..198fc48b5 --- /dev/null +++ b/tracker/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TrackerAppConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' + name = 'tracker' + verbose_name = 'Donation Tracker' diff --git a/tracker/forms.py b/tracker/forms.py index e35feb0b7..2604155f8 100644 --- a/tracker/forms.py +++ b/tracker/forms.py @@ -89,13 +89,6 @@ def __init__(self, event=None, *args, **kwargs): min_value=minDonationAmount, max_value=Decimal('100000'), label='Donation Amount (min ${0})'.format(minDonationAmount), - widget=tracker.widgets.NumberInput( - attrs={ - 'id': 'iDonationAmount', - 'min': str(minDonationAmount), - 'step': '0.01', - } - ), required=True, ) self.fields['comment'] = forms.CharField(widget=forms.Textarea, required=False) @@ -134,24 +127,14 @@ def clean(self): class DonationBidForm(forms.Form): - bid = forms.fields.IntegerField( - label='', - required=True, - widget=tracker.widgets.MegaFilterWidget(model='bidtarget'), - ) + bid = forms.fields.IntegerField(label='', required=True,) customoptionname = forms.fields.CharField( max_length=models.Bid._meta.get_field('name').max_length, label='New Option Name:', required=False, ) amount = forms.DecimalField( - decimal_places=2, - max_digits=20, - required=True, - validators=[positive, nonzero], - widget=tracker.widgets.NumberInput( - attrs={'class': 'cdonationbidamount', 'step': '0.01'} - ), + decimal_places=2, max_digits=20, required=True, validators=[positive, nonzero], ) def clean_bid(self): diff --git a/tracker/migrations/0005_add_event_hashtag.py b/tracker/migrations/0005_add_event_hashtag.py index d97494d9f..01b78855e 100644 --- a/tracker/migrations/0005_add_event_hashtag.py +++ b/tracker/migrations/0005_add_event_hashtag.py @@ -23,6 +23,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='short', - field=models.CharField(help_text='This must be unique, as it is used for slugs.', max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z', 32), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')]), + field=models.CharField(help_text='This must be unique, as it is used for slugs.', max_length=64, unique=True, validators=[django.core.validators.validate_slug]), ), ] diff --git a/tracker/migrations/0020_add_pinned_to_bid.py b/tracker/migrations/0020_add_pinned_to_bid.py new file mode 100644 index 000000000..17505b015 --- /dev/null +++ b/tracker/migrations/0020_add_pinned_to_bid.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-01-04 03:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tracker', '0019_add_pinned_to_donation'), + ] + + operations = [ + migrations.AddField( + model_name='bid', + name='pinned', + field=models.BooleanField(default=False, help_text='Will always show up in the current feeds'), + ), + ] diff --git a/tracker/migrations/0021_merge_20210628_2126.py b/tracker/migrations/0021_merge_20210628_2126.py new file mode 100644 index 000000000..3a28896d2 --- /dev/null +++ b/tracker/migrations/0021_merge_20210628_2126.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.19 on 2021-06-28 21:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tracker', '0020_add_milestone'), + ('tracker', '0020_add_pinned_to_bid'), + ] + + operations = [ + ] diff --git a/tracker/migrations/0026_merge_20220803_2308.py b/tracker/migrations/0026_merge_20220803_2308.py new file mode 100644 index 000000000..81cd671e8 --- /dev/null +++ b/tracker/migrations/0026_merge_20220803_2308.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.20 on 2022-08-03 14:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tracker', '0025_add_socials_to_runner'), + ('tracker', '0021_merge_20210628_2126'), + ] + + operations = [ + ] diff --git a/tracker/models/bid.py b/tracker/models/bid.py index 4d6e86110..d874827f1 100644 --- a/tracker/models/bid.py +++ b/tracker/models/bid.py @@ -1,6 +1,7 @@ from datetime import datetime from decimal import Decimal from gettext import gettext as _ +import logging import mptt.models import pytz @@ -11,6 +12,7 @@ from django.dispatch import receiver from django.urls import reverse +from tracker.analytics import analytics, AnalyticsEventTypes from tracker.validators import positive, nonzero __all__ = [ @@ -19,6 +21,8 @@ 'BidSuggestion', ] +logger = logging.getLogger(__name__) + class BidManager(models.Manager): def get_by_natural_key(self, event, name, speedrun=None, parent=None): @@ -118,6 +122,9 @@ class Bid(mptt.models.MPTTModel): decimal_places=2, max_digits=20, editable=False, default=Decimal('0.00') ) count = models.IntegerField(editable=False) + pinned = models.BooleanField( + default=False, help_text='Will always show up in the current feeds' + ) class Meta: app_label = 'tracker' @@ -259,6 +266,22 @@ def save(self, *args, skip_parent=False, **kwargs): self.event = self.speedrun.event if self.state in ['OPENED', 'CLOSED'] and not self.revealedtime: self.revealedtime = datetime.utcnow().replace(tzinfo=pytz.utc) + analytics.track( + AnalyticsEventTypes.INCENTIVE_OPENED, + { + 'timestamp': self.revealedtime, + 'bid_id': self.id, + 'event_id': self.event_id, + 'run_id': self.speedrun_id, + 'parent_id': self.parent_id, + 'name': self.name, + 'goal': self.goal, + 'is_target': self.istarget, + 'allow_user_options': self.allowuseroptions, + 'max_option_length': self.option_max_length, + 'dependent_on_id': self.biddependency, + }, + ) if self.biddependency: self.event = self.biddependency.event if not self.speedrun: @@ -266,7 +289,7 @@ def save(self, *args, skip_parent=False, **kwargs): self.update_total() super(Bid, self).save(*args, **kwargs) if self.pk: - for option in self.get_descendants(): + for option in self.get_children(): if option.check_parent(): option.save(skip_parent=True) if self.parent and not skip_parent: @@ -283,6 +306,9 @@ def check_parent(self): if self.state not in ['PENDING', 'DENIED'] and self.state != self.parent.state: self.state = self.parent.state changed = True + if self.pinned != self.parent.pinned: + self.pinned = self.parent.pinned + changed = True return changed @property @@ -303,7 +329,7 @@ def update_total(self): self.count = self.bids.filter( donation__transactionstate='COMPLETED' ).count() - # auto close this if it's a challenge with no children and the goal's been met + # auto close and unpin this if it's a challenge with no children and the goal's been met if ( self.goal and self.state == 'OPENED' @@ -311,6 +337,25 @@ def update_total(self): and self.istarget ): self.state = 'CLOSED' + self.pinned = False + analytics.track( + AnalyticsEventTypes.INCENTIVE_MET, + { + 'timestamp': datetime.utcnow(), + 'bid_id': self.pk, + 'event_id': self.event_id, + 'run_id': self.speedrun_id, + 'parent_id': self.parent_id, + 'name': self.name, + 'goal': self.goal, + 'is_target': self.istarget, + 'total_raised': self.total, + 'unique_donations': self.count, + 'allow_user_options': self.allowuseroptions, + 'max_option_length': self.option_max_length, + 'dependent_on_id': self.biddependency, + }, + ) else: options = self.options.exclude(state__in=('DENIED', 'PENDING')).aggregate( Sum('total'), Sum('count') @@ -384,6 +429,31 @@ def clean(self): dependentBid.state = 'OPENED' dependentBid.save() + def save(self, *args, **kwargs): + is_creating = self.pk is None + super(DonationBid, self).save(*args, **kwargs) + # TODO: This should move to `donateviews.process_form` to track bids that + # are created as part of the original donation, and a separate admin view + # to track bids applied manually by a donation processor. + if is_creating: + analytics.track( + AnalyticsEventTypes.BID_APPLIED, + { + 'timestamp': datetime.utcnow(), + 'event_id': self.donation.event_id, + 'incentive_id': self.bid.id, + 'parent_id': self.bid.parent_id, + 'donation_id': self.donation_id, + 'amount': self.amount, + 'total_donation_amount': self.donation.amount, + 'incentive_goal_amount': self.bid.goal, + 'incentive_current_amount': self.bid.total, + # TODO: Set this to an actual value when tracking moves + # to the separate view functions. + 'added_manually': False, + }, + ) + @property def speedrun(self): return self.bid.speedrun diff --git a/tracker/models/donation.py b/tracker/models/donation.py index 6e01bf2b4..f4b2c7c7b 100644 --- a/tracker/models/donation.py +++ b/tracker/models/donation.py @@ -8,9 +8,9 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Count, Sum, Max, Avg +from django.db.models import Count, Sum, Max, Avg, FloatField from django.db.models import signals -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, Cast from django.dispatch import receiver from django.urls import reverse from django.utils import timezone @@ -494,10 +494,10 @@ def update(self): if self.event: aggregate = aggregate.filter(event=self.event) aggregate = aggregate.aggregate( - total=Coalesce(Sum('amount'), 0.0), + total=Cast(Coalesce(Sum('amount'), 0.0), output_field=FloatField()), count=Coalesce(Count('amount'), 0), - max=Coalesce(Max('amount'), 0.0), - avg=Coalesce(Avg('amount'), 0.0), + max=Cast(Coalesce(Max('amount'), 0.0), output_field=FloatField()), + avg=Cast(Coalesce(Avg('amount'), 0.0), output_field=FloatField()), ) self.donation_total = aggregate['total'] self.donation_count = aggregate['count'] diff --git a/tracker/paypalutil.py b/tracker/paypalutil.py index 158d9f909..edd4d30b0 100644 --- a/tracker/paypalutil.py +++ b/tracker/paypalutil.py @@ -60,7 +60,7 @@ def verify_ipn_recipient_email(ipn, email): recipient_email = ipn.business if ipn.business else ipn.receiver_email if recipient_email.lower() != email.lower(): raise SpoofedIPNException( - "IPN receiver %s doesn't match %s".format(recipient_email, email) + f"IPN receiver `{recipient_email}` doesn't match `{email}`" ) @@ -212,12 +212,6 @@ def initialize_paypal_donation(ipnObj): event=donation.event, ) - # Automatically approve anonymous, no-comment donations if an auto-approve - # threshold is set. - auto_min = donation.event.auto_approve_threshold - if auto_min: - donation.approve_if_anonymous_and_no_comment(auto_min) - donation.save() # I think we only care if the _donation_ was freshly created return donation diff --git a/tracker/search_feeds.py b/tracker/search_feeds.py index cc28bdcba..b2c3110d4 100644 --- a/tracker/search_feeds.py +++ b/tracker/search_feeds.py @@ -77,15 +77,8 @@ def get_future_runs(**kwargs): return get_upcoming_runs(include_current=False, **kwargs) -# TODO: why is this so complicated def upcoming_bid_filter(**kwargs): - runs = [ - run.id - for run in get_upcoming_runs( - SpeedRun.objects.filter(Q(bids__state='OPENED')).distinct(), **kwargs - ) - ] - return Q(speedrun__in=runs) + return Q(speedrun__in=(run.id for run in get_upcoming_runs(**kwargs))) def get_upcoming_bids(**kwargs): @@ -195,8 +188,12 @@ def run_feed_filter(feed_name, noslice, params, query): def feed_params(noslice, params, init=None): call_params = init or {} + if 'max_runs' in params: + call_params['max_runs'] = int(params['max_runs']) if 'maxRuns' in params: call_params['max_runs'] = int(params['maxRuns']) + if 'min_runs' in params: + call_params['min_runs'] = int(params['min_runs']) if 'minRuns' in params: call_params['min_runs'] = int(params['minRuns']) if noslice: @@ -219,12 +216,18 @@ def bid_feed_filter(feed_name, noslice, params, query, user): elif feed_name == 'closed': query = query.filter(state='CLOSED') elif feed_name == 'current': - query = query.filter(state='OPENED').filter( - upcoming_bid_filter(**feed_params(noslice, params)) + query = query.filter( + Q(state='OPENED') + & (upcoming_bid_filter(**feed_params(noslice, params)) | Q(pinned=True)) + ) + elif feed_name == 'current_plus': + query = query.filter( + Q(state__in=['OPENED', 'CLOSED']) + & (upcoming_bid_filter(**feed_params(noslice, params)) | Q(pinned=True)) ) elif feed_name == 'future': - query = query.filter(state='OPENED').filter( - future_bid_filter(**feed_params(noslice, params)) + query = query.filter( + Q(state='OPENED') & future_bid_filter(**feed_params(noslice, params)) ) elif feed_name == 'pending': if not user.has_perm('tracker.view_hidden_bid'): diff --git a/tracker/static/ProcessingNode.js b/tracker/static/ProcessingNode.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/tracker/static/donationbids.js b/tracker/static/donationbids.js deleted file mode 100644 index 1febcaa7d..000000000 --- a/tracker/static/donationbids.js +++ /dev/null @@ -1,217 +0,0 @@ -/* eslint-disable */ - -__BIDS__ = null; - -function MegaFilter(objects, groupings, searchFields, labelCallback, detailsCallback) { - - this.objects = objects; - - - this.objectsLookup = {}; - - for (var i in this.objects) { - this.objectsLookup[this.objects[i]['id']] = this.objects[i]; - } - - this.groupings = groupings; - this.searchFields = searchFields; - this.labelCallback = labelCallback; - this.detailsCallback = detailsCallback; - - this.getObjectById = function(id) { - return this.objectsLookup[id]; - } - - this.filterSelectionClosure = function(textBox, typeBox, selectBox) { - - var self = this; - - if (typeBox != null && this.groupings != null && this.groupings.length > 0) { - typeBox.options.length = 0; - typeBox.add(new Option("All", "all")); - for (var i in this.groupings) { - typeBox.add(new Option(this.groupings[i], this.groupings[i])); - } - typeBox.selectedIndex = 0; - } - - return function(event) { - - var typeStr = "all"; - - if (typeBox.selectedIndex > 0) { - typeStr = typeBox.options[typeBox.selectedIndex].value; - - if ($.inArray(typeStr,self.groupings) == -1) { - typeStr = "all"; - } - } - - var tokens = $.trim(textBox.value).split(new RegExp("\\s+")); - for (var tok in tokens) { - tokens[tok] = new RegExp($.ui.autocomplete.escapeRegex(tokens[tok]), "i"); - } - - selectBox.options.length = 0; - - for (var i = 0; i < self.objects.length; ++i) { - var bid = self.objects[i]; - var allFound = true; - - if (typeStr == "all" || typeStr in bid) - { - - for (var tokenIdx in tokens) { - var token = tokens[tokenIdx]; - var found = false; - - var prefix = ""; - - if (typeStr != "all") { - for (var suggestionIdx in bid[typeStr]) { - var suggestion = bid[typeStr][suggestionIdx]; - if (token.test(suggestion)) { - found = true; - } - } - } - - var curBid = bid; - - while (curBid != null && !found) - { - for (var fieldIdx in self.searchFields) { - var field = self.searchFields[fieldIdx]; - - if (field in curBid && token.test(curBid[field])) { - found = true; - break; - } - } - - if ('parent' in curBid) { - curBid = curBid['parent']; - } else { - curBid = null; - } - } - - if (!found) { - allFound = false; - break; - } - } - - if (allFound) { - var prefix = ""; - - if (typeStr != "all") { - prefix = "(" + bid[typeStr] + ") "; - } - - selectBox.options[selectBox.options.length] = new Option(prefix + self.labelCallback(bid), i); - } - } - } - - } - }; - - this.selectionClosure = function(selectBox, descBox, idInput, selectionCallback) { - var self = this; - - return function(event) { - var bid = self.objects[selectBox.options[selectBox.selectedIndex].value]; - - var text = self.detailsCallback(bid); - - $(descBox).html(text); - - var clearButton = $('').get(0); - - $(clearButton).click( - function() { - selectBox.selectedIndex = -1; - $(descBox).html(""); - $(idInput).val(""); - $(idInput).change(); - }); - - $(descBox).append($('
')).append(clearButton); - - $(idInput).val(bid['id']); - $(idInput).change(); - - if (selectionCallback) { - selectionCallback(bid); - } - } - }; - - this.setWidgetValue = function(obj, value) { - var filterBox = $(obj).children(".mf_filter").get(0); - var groupBox = $(obj).children(".mf_grouping").get(0); - var groupBoxLabel = $(obj).children(".mf_groupingLabel").get(0); - var selectBox = $(obj).children(".mf_selectbox").get(0); - var descBox = $(obj).children(".mf_description").get(0); - var idInput = $(obj).children(".mf_selection").get(0); - - var found = false; - - if (value !== 'undefined') { - for (var optionId in selectBox.options) { - var obj = this.objects[optionId]; - if (typeof obj !== 'undefined' && value == obj.id) { - console.log('found'); - selectBox.selectedIndex = optionId; - $(selectBox).change(); - found = true; - break; - } - } - - if (!found) { - $(idInput).val(""); - } - } - } - - this.applyToWidget = function(obj, selectionCallback) { - - var filterBox = $(obj).children(".mf_filter").get(0); - var groupBox = $(obj).children(".mf_grouping").get(0); - var groupBoxLabel = $(obj).children(".mf_groupingLabel").get(0); - var selectBox = $(obj).children(".mf_selectbox").get(0); - var descBox = $(obj).children(".mf_description").get(0); - var idInput = $(obj).children(".mf_selection").get(0); - - if ((this.groupings == null || this.groupings.length == 0) || groupBox == null) { - - if (groupBox != null) { - $(groupBox).hide(); - } - if (groupBoxLabel != null) { - $(groupBoxLabel).hide(); - } - } - - // Its important to unbind any previous events, since the way that django - // dyanmic formset creation works, it will still have the old events attached. - $(filterBox).unbind(); - var filterSelectionMethod = this.filterSelectionClosure(filterBox, groupBox, selectBox); - - $(groupBox).unbind(); - $(groupBox).change(filterSelectionMethod); - $(filterBox).bind("keyup input", filterSelectionMethod); - - filterSelectionMethod(null); - - var optionSelectionMethod = this.selectionClosure(selectBox, descBox, idInput, selectionCallback); - - $(selectBox).unbind(); - $(selectBox).change(optionSelectionMethod); - - this.setWidgetValue(obj, $(idInput).val()); - }; - -} // class MegaFilter diff --git a/tracker/static/jquery-ui.css b/tracker/static/jquery-ui.css deleted file mode 100644 index be07d681e..000000000 --- a/tracker/static/jquery-ui.css +++ /dev/null @@ -1,405 +0,0 @@ -/* -* jQuery UI CSS Framework -* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) -* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. -*/ - -/* Layout helpers -----------------------------------*/ -.ui-helper-hidden { display: none; } -.ui-helper-hidden-accessible { position: absolute; left: -99999999px; } -.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } -.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } -.ui-helper-clearfix { display: inline-block; } -/* required comment for clearfix to work in Opera \*/ -* html .ui-helper-clearfix { height:1%; } -.ui-helper-clearfix { display:block; } -/* end clearfix */ -.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } - - -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { cursor: default !important; } - - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } - - -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } - -/* -* jQuery UI CSS Framework -* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) -* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px -*/ - - -/* Component containers -----------------------------------*/ -.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; } -.ui-widget .ui-widget { font-size: 1em; } -.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } -.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; } -.ui-widget-content a { color: #222222; } -.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; } -.ui-widget-header a { color: #222222; } - -/* Interaction states -----------------------------------*/ -.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; outline: none; } -.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; outline: none; } -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; } -.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; outline: none; } -.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; } -.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; outline: none; text-decoration: none; } - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; } -.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636; } -.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; } -.ui-state-error a, .ui-widget-content .ui-state-error a { color: #cd0a0a; } -.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #cd0a0a; } -.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } -.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; } -.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } -.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } -.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } -.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); } -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } -.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } -.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); } -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); } - -/* positioning */ -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-off { background-position: -96px -144px; } -.ui-icon-radio-on { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; } -.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; } -.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; } -.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; } -.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; } -.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; } -.ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; } -.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; } -.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; } - -/* Overlays */ -.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); } -.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; }/* Accordion -----------------------------------*/ -.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } -.ui-accordion .ui-accordion-li-fix { display: inline; } -.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } -.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em 2.2em; } -.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } -.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; } -.ui-accordion .ui-accordion-content-active { display: block; }/* Datepicker -----------------------------------*/ -.ui-datepicker { width: 17em; padding: .2em .2em 0; } -.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } -.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } -.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } -.ui-datepicker .ui-datepicker-prev { left:2px; } -.ui-datepicker .ui-datepicker-next { right:2px; } -.ui-datepicker .ui-datepicker-prev-hover { left:1px; } -.ui-datepicker .ui-datepicker-next-hover { right:1px; } -.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } -.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } -.ui-datepicker .ui-datepicker-title select { float:left; font-size:1em; margin:1px 0; } -.ui-datepicker select.ui-datepicker-month-year {width: 100%;} -.ui-datepicker select.ui-datepicker-month, -.ui-datepicker select.ui-datepicker-year { width: 49%;} -.ui-datepicker .ui-datepicker-title select.ui-datepicker-year { float: right; } -.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } -.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } -.ui-datepicker td { border: 0; padding: 1px; } -.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } -.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } -.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } -.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } - -/* with multiple calendars */ -.ui-datepicker.ui-datepicker-multi { width:auto; } -.ui-datepicker-multi .ui-datepicker-group { float:left; } -.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } -.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } -.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } -.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } -.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } -.ui-datepicker-row-break { clear:both; width:100%; } - -/* RTL support */ -.ui-datepicker-rtl { direction: rtl; } -.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } -.ui-datepicker-rtl .ui-datepicker-group { float:right; } -.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } -.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } - -/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ -.ui-datepicker-cover { - display: none; /*sorry for IE5*/ - display/**/: block; /*sorry for IE5*/ - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ -}/* Dialog -----------------------------------*/ -.ui-dialog { position: relative; padding: .2em; width: 300px; } -.ui-dialog .ui-dialog-titlebar { padding: .5em .3em .3em 1em; position: relative; } -.ui-dialog .ui-dialog-title { float: left; margin: .1em 0 .2em; } -.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } -.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } -.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } -.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } -.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } -.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; } -.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } -.ui-draggable .ui-dialog-titlebar { cursor: move; } -/* Progressbar -----------------------------------*/ -.ui-progressbar { height:2em; text-align: left; } -.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }/* Resizable -----------------------------------*/ -.ui-resizable { position: relative;} -.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;} -.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } -.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0px; } -.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0px; } -.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0px; height: 100%; } -.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0px; height: 100%; } -.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } -.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } -.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } -.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Slider -----------------------------------*/ -.ui-slider { position: relative; text-align: left; } -.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } -.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; } - -.ui-slider-horizontal { height: .8em; } -.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } -.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } -.ui-slider-horizontal .ui-slider-range-min { left: 0; } -.ui-slider-horizontal .ui-slider-range-max { right: 0; } - -.ui-slider-vertical { width: .8em; height: 100px; } -.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } -.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } -.ui-slider-vertical .ui-slider-range-min { bottom: 0; } -.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs -----------------------------------*/ -.ui-tabs { padding: .2em; zoom: 1; } -.ui-tabs .ui-tabs-nav { list-style: none; position: relative; padding: .2em .2em 0; } -.ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; } -.ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: 1px; border-bottom-width: 0; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } -.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ -.ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; } -.ui-tabs .ui-tabs-hide { display: none !important; } diff --git a/tracker/static/jquery-ui.js b/tracker/static/jquery-ui.js deleted file mode 100644 index 7b727e743..000000000 --- a/tracker/static/jquery-ui.js +++ /dev/null @@ -1,15003 +0,0 @@ -/*! jQuery UI - v1.10.3 - 2013-05-03 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.sortable.js, jquery.ui.effect.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js, jquery.ui.menu.js, jquery.ui.position.js, jquery.ui.progressbar.js, jquery.ui.slider.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js -* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ -(function( $, undefined ) { - -var uuid = 0, - runiqueId = /^ui-id-\d+$/; - -// $.ui might exist from components with no dependencies, e.g., $.ui.position -$.ui = $.ui || {}; - -$.extend( $.ui, { - version: "1.10.3", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - NUMPAD_ADD: 107, - NUMPAD_DECIMAL: 110, - NUMPAD_DIVIDE: 111, - NUMPAD_ENTER: 108, - NUMPAD_MULTIPLY: 106, - NUMPAD_SUBTRACT: 109, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - scrollParent: function() { - var scrollParent; - if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { - scrollParent = this.parents().filter(function() { - return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } else { - scrollParent = this.parents().filter(function() { - return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } - - return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - //
- value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - }, - - uniqueId: function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + (++uuid); - } - }); - }, - - removeUniqueId: function() { - return this.each(function() { - if ( runiqueId.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap=#" + mapName + "]" )[0]; - return !!img && visible( img ); - } - return ( /input|select|textarea|button|object/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - - - - - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.support.selectstart = "onselectstart" in document.createElement( "div" ); -$.fn.extend({ - disableSelection: function() { - return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + - ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }, - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - } -}); - -$.extend( $.ui, { - // $.ui.plugin is deprecated. Use $.widget() extensions instead. - plugin: { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args ) { - var i, - set = instance.plugins[ name ]; - if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } - }, - - // only used by resizable - hasScroll: function( el, a ) { - - //If overflow is hidden, the element might have extra content, but the user wants to hide it - if ( $( el ).css( "overflow" ) === "hidden") { - return false; - } - - var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", - has = false; - - if ( el[ scroll ] > 0 ) { - return true; - } - - // TODO: determine which cases actually cause this to happen - // if the element doesn't have the scroll set, see if it's possible to - // set the scroll - el[ scroll ] = 1; - has = ( el[ scroll ] > 0 ); - el[ scroll ] = 0; - return has; - } -}); - -})( jQuery ); - -(function( $, undefined ) { - -var uuid = 0, - slice = Array.prototype.slice, - _cleanData = $.cleanData; -$.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); -}; - -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); -}; - -$.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; - -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), - returnValue = this; - - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.widget.extend.apply( null, [ options ].concat(args) ) : - options; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} )._init(); - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "
", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( value === undefined ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( value === undefined ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - - return this; - }, - - enable: function() { - return this._setOption( "disabled", false ); - }, - disable: function() { - return this._setOption( "disabled", true ); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - // accept selectors, DOM elements - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^(\w+)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; - -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); - -})( jQuery ); - -(function( $, undefined ) { - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -$.widget("ui.mouse", { - version: "1.10.3", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown."+this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click."+this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("."+this.widgetName); - if ( this._mouseMoveDelegate ) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if( mouseHandled ) { return; } - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - $(document) - .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .bind("mouseup."+this.widgetName, this._mouseUpDelegate); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - -})(jQuery); - -(function( $, undefined ) { - -$.widget("ui.draggable", $.ui.mouse, { - version: "1.10.3", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { - this.element[0].style.position = "relative"; - } - if (this.options.addClasses){ - this.element.addClass("ui-draggable"); - } - if (this.options.disabled){ - this.element.addClass("ui-draggable-disabled"); - } - - this._mouseInit(); - - }, - - _destroy: function() { - this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); - this._mouseDestroy(); - }, - - _mouseCapture: function(event) { - - var o = this.options; - - // among others, prevent a drag on a resizable-handle - if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { - return false; - } - - //Quit if we're not on a valid handle - this.handle = this._getHandle(event); - if (!this.handle) { - return false; - } - - $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { - $("
") - .css({ - width: this.offsetWidth+"px", height: this.offsetHeight+"px", - position: "absolute", opacity: "0.001", zIndex: 1000 - }) - .css($(this).offset()) - .appendTo("body"); - }); - - return true; - - }, - - _mouseStart: function(event) { - - var o = this.options; - - //Create and append the visible helper - this.helper = this._createHelper(event); - - this.helper.addClass("ui-draggable-dragging"); - - //Cache the helper size - this._cacheHelperProportions(); - - //If ddmanager is used for droppables, set the global draggable - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Store the helper's css position - this.cssPosition = this.helper.css( "position" ); - this.scrollParent = this.helper.scrollParent(); - this.offsetParent = this.helper.offsetParent(); - this.offsetParentCssPosition = this.offsetParent.css( "position" ); - - //The element's absolute position on the page minus margins - this.offset = this.positionAbs = this.element.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - //Reset scroll cache - this.offset.scroll = false; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - //Generate the original position - this.originalPosition = this.position = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Set a containment if given in the options - this._setContainment(); - - //Trigger event + callbacks - if(this._trigger("start", event) === false) { - this._clear(); - return false; - } - - //Recache the helper size - this._cacheHelperProportions(); - - //Prepare the droppable offsets - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - - this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position - - //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStart(this, event); - } - - return true; - }, - - _mouseDrag: function(event, noPropagation) { - // reset any necessary cached properties (see #5009) - if ( this.offsetParentCssPosition === "fixed" ) { - this.offset.parent = this._getParentOffset(); - } - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - //Call plugins and callbacks and use the resulting position if something is returned - if (!noPropagation) { - var ui = this._uiHash(); - if(this._trigger("drag", event, ui) === false) { - this._mouseUp({}); - return false; - } - this.position = ui.position; - } - - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - return false; - }, - - _mouseStop: function(event) { - - //If we are using droppables, inform the manager about the drop - var that = this, - dropped = false; - if ($.ui.ddmanager && !this.options.dropBehaviour) { - dropped = $.ui.ddmanager.drop(this, event); - } - - //if a drop comes from outside (a sortable) - if(this.dropped) { - dropped = this.dropped; - this.dropped = false; - } - - //if the original element is no longer in the DOM don't bother to continue (see #8269) - if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { - return false; - } - - if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { - $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { - if(that._trigger("stop", event) !== false) { - that._clear(); - } - }); - } else { - if(this._trigger("stop", event) !== false) { - this._clear(); - } - } - - return false; - }, - - _mouseUp: function(event) { - //Remove frame helpers - $("div.ui-draggable-iframeFix").each(function() { - this.parentNode.removeChild(this); - }); - - //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) - if( $.ui.ddmanager ) { - $.ui.ddmanager.dragStop(this, event); - } - - return $.ui.mouse.prototype._mouseUp.call(this, event); - }, - - cancel: function() { - - if(this.helper.is(".ui-draggable-dragging")) { - this._mouseUp({}); - } else { - this._clear(); - } - - return this; - - }, - - _getHandle: function(event) { - return this.options.handle ? - !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : - true; - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); - - if(!helper.parents("body").length) { - helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); - } - - if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { - helper.css("position", "absolute"); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - //Get the offsetParent and cache its position - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - //This needs to be actually done for all browsers, since pageX/pageY includes this information - //Ugly IE fix - if((this.offsetParent[0] === document.body) || - (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.element.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.element.css("marginLeft"),10) || 0), - top: (parseInt(this.element.css("marginTop"),10) || 0), - right: (parseInt(this.element.css("marginRight"),10) || 0), - bottom: (parseInt(this.element.css("marginBottom"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var over, c, ce, - o = this.options; - - if ( !o.containment ) { - this.containment = null; - return; - } - - if ( o.containment === "window" ) { - this.containment = [ - $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, - $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, - $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, - $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment === "document") { - this.containment = [ - 0, - 0, - $( document ).width() - this.helperProportions.width - this.margins.left, - ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment.constructor === Array ) { - this.containment = o.containment; - return; - } - - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } - - c = $( o.containment ); - ce = c[ 0 ]; - - if( !ce ) { - return; - } - - over = c.css( "overflow" ) !== "hidden"; - - this.containment = [ - ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), - ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , - ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, - ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom - ]; - this.relative_container = c; - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) - ) - }; - - }, - - _generatePosition: function(event) { - - var containment, co, top, left, - o = this.options, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, - pageX = event.pageX, - pageY = event.pageY; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - // If we are not dragging yet, we won't check for options - if ( this.originalPosition ) { - if ( this.containment ) { - if ( this.relative_container ){ - co = this.relative_container.offset(); - containment = [ - this.containment[ 0 ] + co.left, - this.containment[ 1 ] + co.top, - this.containment[ 2 ] + co.left, - this.containment[ 3 ] + co.top - ]; - } - else { - containment = this.containment; - } - - if(event.pageX - this.offset.click.left < containment[0]) { - pageX = containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < containment[1]) { - pageY = containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > containment[2]) { - pageX = containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > containment[3]) { - pageY = containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) - top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; - pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; - pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) - ) - }; - - }, - - _clear: function() { - this.helper.removeClass("ui-draggable-dragging"); - if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { - this.helper.remove(); - } - this.helper = null; - this.cancelHelperRemoval = false; - }, - - // From now on bulk stuff - mainly helpers - - _trigger: function(type, event, ui) { - ui = ui || this._uiHash(); - $.ui.plugin.call(this, type, [event, ui]); - //The absolute position has to be recalculated after plugins - if(type === "drag") { - this.positionAbs = this._convertPositionTo("absolute"); - } - return $.Widget.prototype._trigger.call(this, type, event, ui); - }, - - plugins: {}, - - _uiHash: function() { - return { - helper: this.helper, - position: this.position, - originalPosition: this.originalPosition, - offset: this.positionAbs - }; - } - -}); - -$.ui.plugin.add("draggable", "connectToSortable", { - start: function(event, ui) { - - var inst = $(this).data("ui-draggable"), o = inst.options, - uiSortable = $.extend({}, ui, { item: inst.element }); - inst.sortables = []; - $(o.connectToSortable).each(function() { - var sortable = $.data(this, "ui-sortable"); - if (sortable && !sortable.options.disabled) { - inst.sortables.push({ - instance: sortable, - shouldRevert: sortable.options.revert - }); - sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). - sortable._trigger("activate", event, uiSortable); - } - }); - - }, - stop: function(event, ui) { - - //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper - var inst = $(this).data("ui-draggable"), - uiSortable = $.extend({}, ui, { item: inst.element }); - - $.each(inst.sortables, function() { - if(this.instance.isOver) { - - this.instance.isOver = 0; - - inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance - this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) - - //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" - if(this.shouldRevert) { - this.instance.options.revert = this.shouldRevert; - } - - //Trigger the stop of the sortable - this.instance._mouseStop(event); - - this.instance.options.helper = this.instance.options._helper; - - //If the helper has been the original item, restore properties in the sortable - if(inst.options.helper === "original") { - this.instance.currentItem.css({ top: "auto", left: "auto" }); - } - - } else { - this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance - this.instance._trigger("deactivate", event, uiSortable); - } - - }); - - }, - drag: function(event, ui) { - - var inst = $(this).data("ui-draggable"), that = this; - - $.each(inst.sortables, function() { - - var innermostIntersecting = false, - thisSortable = this; - - //Copy over some variables to allow calling the sortable's native _intersectsWith - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - - if(this.instance._intersectsWith(this.instance.containerCache)) { - innermostIntersecting = true; - $.each(inst.sortables, function () { - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - if (this !== thisSortable && - this.instance._intersectsWith(this.instance.containerCache) && - $.contains(thisSortable.instance.element[0], this.instance.element[0]) - ) { - innermostIntersecting = false; - } - return innermostIntersecting; - }); - } - - - if(innermostIntersecting) { - //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once - if(!this.instance.isOver) { - - this.instance.isOver = 1; - //Now we fake the start of dragging for the sortable instance, - //by cloning the list group item, appending it to the sortable and using it as inst.currentItem - //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) - this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); - this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it - this.instance.options.helper = function() { return ui.helper[0]; }; - - event.target = this.instance.currentItem[0]; - this.instance._mouseCapture(event, true); - this.instance._mouseStart(event, true, true); - - //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes - this.instance.offset.click.top = inst.offset.click.top; - this.instance.offset.click.left = inst.offset.click.left; - this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; - this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; - - inst._trigger("toSortable", event); - inst.dropped = this.instance.element; //draggable revert needs that - //hack so receive/update callbacks work (mostly) - inst.currentItem = inst.element; - this.instance.fromOutside = inst; - - } - - //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable - if(this.instance.currentItem) { - this.instance._mouseDrag(event); - } - - } else { - - //If it doesn't intersect with the sortable, and it intersected before, - //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval - if(this.instance.isOver) { - - this.instance.isOver = 0; - this.instance.cancelHelperRemoval = true; - - //Prevent reverting on this forced stop - this.instance.options.revert = false; - - // The out event needs to be triggered independently - this.instance._trigger("out", event, this.instance._uiHash(this.instance)); - - this.instance._mouseStop(event, true); - this.instance.options.helper = this.instance.options._helper; - - //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size - this.instance.currentItem.remove(); - if(this.instance.placeholder) { - this.instance.placeholder.remove(); - } - - inst._trigger("fromSortable", event); - inst.dropped = false; //draggable revert needs that - } - - } - - }); - - } -}); - -$.ui.plugin.add("draggable", "cursor", { - start: function() { - var t = $("body"), o = $(this).data("ui-draggable").options; - if (t.css("cursor")) { - o._cursor = t.css("cursor"); - } - t.css("cursor", o.cursor); - }, - stop: function() { - var o = $(this).data("ui-draggable").options; - if (o._cursor) { - $("body").css("cursor", o._cursor); - } - } -}); - -$.ui.plugin.add("draggable", "opacity", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("opacity")) { - o._opacity = t.css("opacity"); - } - t.css("opacity", o.opacity); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._opacity) { - $(ui.helper).css("opacity", o._opacity); - } - } -}); - -$.ui.plugin.add("draggable", "scroll", { - start: function() { - var i = $(this).data("ui-draggable"); - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - i.overflowOffset = i.scrollParent.offset(); - } - }, - drag: function( event ) { - - var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; - - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - - if(!o.axis || o.axis !== "x") { - if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; - } - } - - if(!o.axis || o.axis !== "y") { - if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; - } - } - - } else { - - if(!o.axis || o.axis !== "x") { - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - } - - if(!o.axis || o.axis !== "y") { - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(i, event); - } - - } -}); - -$.ui.plugin.add("draggable", "snap", { - start: function() { - - var i = $(this).data("ui-draggable"), - o = i.options; - - i.snapElements = []; - - $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { - var $t = $(this), - $o = $t.offset(); - if(this !== i.element[0]) { - i.snapElements.push({ - item: this, - width: $t.outerWidth(), height: $t.outerHeight(), - top: $o.top, left: $o.left - }); - } - }); - - }, - drag: function(event, ui) { - - var ts, bs, ls, rs, l, r, t, b, i, first, - inst = $(this).data("ui-draggable"), - o = inst.options, - d = o.snapTolerance, - x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, - y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; - - for (i = inst.snapElements.length - 1; i >= 0; i--){ - - l = inst.snapElements[i].left; - r = l + inst.snapElements[i].width; - t = inst.snapElements[i].top; - b = t + inst.snapElements[i].height; - - if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { - if(inst.snapElements[i].snapping) { - (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = false; - continue; - } - - if(o.snapMode !== "inner") { - ts = Math.abs(t - y2) <= d; - bs = Math.abs(b - y1) <= d; - ls = Math.abs(l - x2) <= d; - rs = Math.abs(r - x1) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; - } - } - - first = (ts || bs || ls || rs); - - if(o.snapMode !== "outer") { - ts = Math.abs(t - y1) <= d; - bs = Math.abs(b - y2) <= d; - ls = Math.abs(l - x1) <= d; - rs = Math.abs(r - x2) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; - } - } - - if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { - (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = (ts || bs || ls || rs || first); - - } - - } -}); - -$.ui.plugin.add("draggable", "stack", { - start: function() { - var min, - o = this.data("ui-draggable").options, - group = $.makeArray($(o.stack)).sort(function(a,b) { - return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); - }); - - if (!group.length) { return; } - - min = parseInt($(group[0]).css("zIndex"), 10) || 0; - $(group).each(function(i) { - $(this).css("zIndex", min + i); - }); - this.css("zIndex", (min + group.length)); - } -}); - -$.ui.plugin.add("draggable", "zIndex", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("zIndex")) { - o._zIndex = t.css("zIndex"); - } - t.css("zIndex", o.zIndex); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._zIndex) { - $(ui.helper).css("zIndex", o._zIndex); - } - } -}); - -})(jQuery); - -(function( $, undefined ) { - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -$.widget("ui.droppable", { - version: "1.10.3", - widgetEventPrefix: "drop", - options: { - accept: "*", - activeClass: false, - addClasses: true, - greedy: false, - hoverClass: false, - scope: "default", - tolerance: "intersect", - - // callbacks - activate: null, - deactivate: null, - drop: null, - out: null, - over: null - }, - _create: function() { - - var o = this.options, - accept = o.accept; - - this.isover = false; - this.isout = true; - - this.accept = $.isFunction(accept) ? accept : function(d) { - return d.is(accept); - }; - - //Store the droppable's proportions - this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight }; - - // Add the reference and positions to the manager - $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; - $.ui.ddmanager.droppables[o.scope].push(this); - - (o.addClasses && this.element.addClass("ui-droppable")); - - }, - - _destroy: function() { - var i = 0, - drop = $.ui.ddmanager.droppables[this.options.scope]; - - for ( ; i < drop.length; i++ ) { - if ( drop[i] === this ) { - drop.splice(i, 1); - } - } - - this.element.removeClass("ui-droppable ui-droppable-disabled"); - }, - - _setOption: function(key, value) { - - if(key === "accept") { - this.accept = $.isFunction(value) ? value : function(d) { - return d.is(value); - }; - } - $.Widget.prototype._setOption.apply(this, arguments); - }, - - _activate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.addClass(this.options.activeClass); - } - if(draggable){ - this._trigger("activate", event, this.ui(draggable)); - } - }, - - _deactivate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(draggable){ - this._trigger("deactivate", event, this.ui(draggable)); - } - }, - - _over: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.addClass(this.options.hoverClass); - } - this._trigger("over", event, this.ui(draggable)); - } - - }, - - _out: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("out", event, this.ui(draggable)); - } - - }, - - _drop: function(event,custom) { - - var draggable = custom || $.ui.ddmanager.current, - childrenIntersection = false; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return false; - } - - this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { - var inst = $.data(this, "ui-droppable"); - if( - inst.options.greedy && - !inst.options.disabled && - inst.options.scope === draggable.options.scope && - inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && - $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) - ) { childrenIntersection = true; return false; } - }); - if(childrenIntersection) { - return false; - } - - if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("drop", event, this.ui(draggable)); - return this.element; - } - - return false; - - }, - - ui: function(c) { - return { - draggable: (c.currentItem || c.element), - helper: c.helper, - position: c.position, - offset: c.positionAbs - }; - } - -}); - -$.ui.intersect = function(draggable, droppable, toleranceMode) { - - if (!droppable.offset) { - return false; - } - - var draggableLeft, draggableTop, - x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width, - y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height, - l = droppable.offset.left, r = l + droppable.proportions.width, - t = droppable.offset.top, b = t + droppable.proportions.height; - - switch (toleranceMode) { - case "fit": - return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); - case "intersect": - return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half - x2 - (draggable.helperProportions.width / 2) < r && // Left Half - t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half - y2 - (draggable.helperProportions.height / 2) < b ); // Top Half - case "pointer": - draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); - draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); - return isOverAxis( draggableTop, t, droppable.proportions.height ) && isOverAxis( draggableLeft, l, droppable.proportions.width ); - case "touch": - return ( - (y1 >= t && y1 <= b) || // Top edge touching - (y2 >= t && y2 <= b) || // Bottom edge touching - (y1 < t && y2 > b) // Surrounded vertically - ) && ( - (x1 >= l && x1 <= r) || // Left edge touching - (x2 >= l && x2 <= r) || // Right edge touching - (x1 < l && x2 > r) // Surrounded horizontally - ); - default: - return false; - } - -}; - -/* - This manager tracks offsets of draggables and droppables -*/ -$.ui.ddmanager = { - current: null, - droppables: { "default": [] }, - prepareOffsets: function(t, event) { - - var i, j, - m = $.ui.ddmanager.droppables[t.options.scope] || [], - type = event ? event.type : null, // workaround for #2317 - list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); - - droppablesLoop: for (i = 0; i < m.length; i++) { - - //No disabled and non-accepted - if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { - continue; - } - - // Filter out elements in the current dragged item - for (j=0; j < list.length; j++) { - if(list[j] === m[i].element[0]) { - m[i].proportions.height = 0; - continue droppablesLoop; - } - } - - m[i].visible = m[i].element.css("display") !== "none"; - if(!m[i].visible) { - continue; - } - - //Activate the droppable if used directly from draggables - if(type === "mousedown") { - m[i]._activate.call(m[i], event); - } - - m[i].offset = m[i].element.offset(); - m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight }; - - } - - }, - drop: function(draggable, event) { - - var dropped = false; - // Create a copy of the droppables in case the list changes during the drop (#9116) - $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { - - if(!this.options) { - return; - } - if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { - dropped = this._drop.call(this, event) || dropped; - } - - if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - this.isout = true; - this.isover = false; - this._deactivate.call(this, event); - } - - }); - return dropped; - - }, - dragStart: function( draggable, event ) { - //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) - draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - }); - }, - drag: function(draggable, event) { - - //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. - if(draggable.options.refreshPositions) { - $.ui.ddmanager.prepareOffsets(draggable, event); - } - - //Run through all droppables and check their positions based on specific tolerance options - $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { - - if(this.options.disabled || this.greedyChild || !this.visible) { - return; - } - - var parentInstance, scope, parent, - intersects = $.ui.intersect(draggable, this, this.options.tolerance), - c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); - if(!c) { - return; - } - - if (this.options.greedy) { - // find droppable parents with same scope - scope = this.options.scope; - parent = this.element.parents(":data(ui-droppable)").filter(function () { - return $.data(this, "ui-droppable").options.scope === scope; - }); - - if (parent.length) { - parentInstance = $.data(parent[0], "ui-droppable"); - parentInstance.greedyChild = (c === "isover"); - } - } - - // we just moved into a greedy child - if (parentInstance && c === "isover") { - parentInstance.isover = false; - parentInstance.isout = true; - parentInstance._out.call(parentInstance, event); - } - - this[c] = true; - this[c === "isout" ? "isover" : "isout"] = false; - this[c === "isover" ? "_over" : "_out"].call(this, event); - - // we just moved out of a greedy child - if (parentInstance && c === "isout") { - parentInstance.isout = false; - parentInstance.isover = true; - parentInstance._over.call(parentInstance, event); - } - }); - - }, - dragStop: function( draggable, event ) { - draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); - //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } -}; - -})(jQuery); - -(function( $, undefined ) { - -function num(v) { - return parseInt(v, 10) || 0; -} - -function isNumber(value) { - return !isNaN(parseInt(value, 10)); -} - -$.widget("ui.resizable", $.ui.mouse, { - version: "1.10.3", - widgetEventPrefix: "resize", - options: { - alsoResize: false, - animate: false, - animateDuration: "slow", - animateEasing: "swing", - aspectRatio: false, - autoHide: false, - containment: false, - ghost: false, - grid: false, - handles: "e,s,se", - helper: false, - maxHeight: null, - maxWidth: null, - minHeight: 10, - minWidth: 10, - // See #7960 - zIndex: 90, - - // callbacks - resize: null, - start: null, - stop: null - }, - _create: function() { - - var n, i, handle, axis, hname, - that = this, - o = this.options; - this.element.addClass("ui-resizable"); - - $.extend(this, { - _aspectRatio: !!(o.aspectRatio), - aspectRatio: o.aspectRatio, - originalElement: this.element, - _proportionallyResizeElements: [], - _helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null - }); - - //Wrap the element if it cannot hold child nodes - if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)) { - - //Create a wrapper element and set the wrapper to the new current internal element - this.element.wrap( - $("
").css({ - position: this.element.css("position"), - width: this.element.outerWidth(), - height: this.element.outerHeight(), - top: this.element.css("top"), - left: this.element.css("left") - }) - ); - - //Overwrite the original this.element - this.element = this.element.parent().data( - "ui-resizable", this.element.data("ui-resizable") - ); - - this.elementIsWrapper = true; - - //Move margins to the wrapper - this.element.css({ marginLeft: this.originalElement.css("marginLeft"), marginTop: this.originalElement.css("marginTop"), marginRight: this.originalElement.css("marginRight"), marginBottom: this.originalElement.css("marginBottom") }); - this.originalElement.css({ marginLeft: 0, marginTop: 0, marginRight: 0, marginBottom: 0}); - - //Prevent Safari textarea resize - this.originalResizeStyle = this.originalElement.css("resize"); - this.originalElement.css("resize", "none"); - - //Push the actual element to our proportionallyResize internal array - this._proportionallyResizeElements.push(this.originalElement.css({ position: "static", zoom: 1, display: "block" })); - - // avoid IE jump (hard set the margin) - this.originalElement.css({ margin: this.originalElement.css("margin") }); - - // fix handlers offset - this._proportionallyResize(); - - } - - this.handles = o.handles || (!$(".ui-resizable-handle", this.element).length ? "e,s,se" : { n: ".ui-resizable-n", e: ".ui-resizable-e", s: ".ui-resizable-s", w: ".ui-resizable-w", se: ".ui-resizable-se", sw: ".ui-resizable-sw", ne: ".ui-resizable-ne", nw: ".ui-resizable-nw" }); - if(this.handles.constructor === String) { - - if ( this.handles === "all") { - this.handles = "n,e,s,w,se,sw,ne,nw"; - } - - n = this.handles.split(","); - this.handles = {}; - - for(i = 0; i < n.length; i++) { - - handle = $.trim(n[i]); - hname = "ui-resizable-"+handle; - axis = $("
"); - - // Apply zIndex to all handles - see #7960 - axis.css({ zIndex: o.zIndex }); - - //TODO : What's going on here? - if ("se" === handle) { - axis.addClass("ui-icon ui-icon-gripsmall-diagonal-se"); - } - - //Insert into internal handles object and append to element - this.handles[handle] = ".ui-resizable-"+handle; - this.element.append(axis); - } - - } - - this._renderAxis = function(target) { - - var i, axis, padPos, padWrapper; - - target = target || this.element; - - for(i in this.handles) { - - if(this.handles[i].constructor === String) { - this.handles[i] = $(this.handles[i], this.element).show(); - } - - //Apply pad to wrapper element, needed to fix axis position (textarea, inputs, scrolls) - if (this.elementIsWrapper && this.originalElement[0].nodeName.match(/textarea|input|select|button/i)) { - - axis = $(this.handles[i], this.element); - - //Checking the correct pad and border - padWrapper = /sw|ne|nw|se|n|s/.test(i) ? axis.outerHeight() : axis.outerWidth(); - - //The padding type i have to apply... - padPos = [ "padding", - /ne|nw|n/.test(i) ? "Top" : - /se|sw|s/.test(i) ? "Bottom" : - /^e$/.test(i) ? "Right" : "Left" ].join(""); - - target.css(padPos, padWrapper); - - this._proportionallyResize(); - - } - - //TODO: What's that good for? There's not anything to be executed left - if(!$(this.handles[i]).length) { - continue; - } - } - }; - - //TODO: make renderAxis a prototype function - this._renderAxis(this.element); - - this._handles = $(".ui-resizable-handle", this.element) - .disableSelection(); - - //Matching axis name - this._handles.mouseover(function() { - if (!that.resizing) { - if (this.className) { - axis = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i); - } - //Axis, default = se - that.axis = axis && axis[1] ? axis[1] : "se"; - } - }); - - //If we want to auto hide the elements - if (o.autoHide) { - this._handles.hide(); - $(this.element) - .addClass("ui-resizable-autohide") - .mouseenter(function() { - if (o.disabled) { - return; - } - $(this).removeClass("ui-resizable-autohide"); - that._handles.show(); - }) - .mouseleave(function(){ - if (o.disabled) { - return; - } - if (!that.resizing) { - $(this).addClass("ui-resizable-autohide"); - that._handles.hide(); - } - }); - } - - //Initialize the mouse interaction - this._mouseInit(); - - }, - - _destroy: function() { - - this._mouseDestroy(); - - var wrapper, - _destroy = function(exp) { - $(exp).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing") - .removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove(); - }; - - //TODO: Unwrap at same DOM position - if (this.elementIsWrapper) { - _destroy(this.element); - wrapper = this.element; - this.originalElement.css({ - position: wrapper.css("position"), - width: wrapper.outerWidth(), - height: wrapper.outerHeight(), - top: wrapper.css("top"), - left: wrapper.css("left") - }).insertAfter( wrapper ); - wrapper.remove(); - } - - this.originalElement.css("resize", this.originalResizeStyle); - _destroy(this.originalElement); - - return this; - }, - - _mouseCapture: function(event) { - var i, handle, - capture = false; - - for (i in this.handles) { - handle = $(this.handles[i])[0]; - if (handle === event.target || $.contains(handle, event.target)) { - capture = true; - } - } - - return !this.options.disabled && capture; - }, - - _mouseStart: function(event) { - - var curleft, curtop, cursor, - o = this.options, - iniPos = this.element.position(), - el = this.element; - - this.resizing = true; - - // bugfix for http://dev.jquery.com/ticket/1749 - if ( (/absolute/).test( el.css("position") ) ) { - el.css({ position: "absolute", top: el.css("top"), left: el.css("left") }); - } else if (el.is(".ui-draggable")) { - el.css({ position: "absolute", top: iniPos.top, left: iniPos.left }); - } - - this._renderProxy(); - - curleft = num(this.helper.css("left")); - curtop = num(this.helper.css("top")); - - if (o.containment) { - curleft += $(o.containment).scrollLeft() || 0; - curtop += $(o.containment).scrollTop() || 0; - } - - //Store needed variables - this.offset = this.helper.offset(); - this.position = { left: curleft, top: curtop }; - this.size = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() }; - this.originalSize = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() }; - this.originalPosition = { left: curleft, top: curtop }; - this.sizeDiff = { width: el.outerWidth() - el.width(), height: el.outerHeight() - el.height() }; - this.originalMousePosition = { left: event.pageX, top: event.pageY }; - - //Aspect Ratio - this.aspectRatio = (typeof o.aspectRatio === "number") ? o.aspectRatio : ((this.originalSize.width / this.originalSize.height) || 1); - - cursor = $(".ui-resizable-" + this.axis).css("cursor"); - $("body").css("cursor", cursor === "auto" ? this.axis + "-resize" : cursor); - - el.addClass("ui-resizable-resizing"); - this._propagate("start", event); - return true; - }, - - _mouseDrag: function(event) { - - //Increase performance, avoid regex - var data, - el = this.helper, props = {}, - smp = this.originalMousePosition, - a = this.axis, - prevTop = this.position.top, - prevLeft = this.position.left, - prevWidth = this.size.width, - prevHeight = this.size.height, - dx = (event.pageX-smp.left)||0, - dy = (event.pageY-smp.top)||0, - trigger = this._change[a]; - - if (!trigger) { - return false; - } - - // Calculate the attrs that will be change - data = trigger.apply(this, [event, dx, dy]); - - // Put this in the mouseDrag handler since the user can start pressing shift while resizing - this._updateVirtualBoundaries(event.shiftKey); - if (this._aspectRatio || event.shiftKey) { - data = this._updateRatio(data, event); - } - - data = this._respectSize(data, event); - - this._updateCache(data); - - // plugins callbacks need to be called first - this._propagate("resize", event); - - if (this.position.top !== prevTop) { - props.top = this.position.top + "px"; - } - if (this.position.left !== prevLeft) { - props.left = this.position.left + "px"; - } - if (this.size.width !== prevWidth) { - props.width = this.size.width + "px"; - } - if (this.size.height !== prevHeight) { - props.height = this.size.height + "px"; - } - el.css(props); - - if (!this._helper && this._proportionallyResizeElements.length) { - this._proportionallyResize(); - } - - // Call the user callback if the element was resized - if ( ! $.isEmptyObject(props) ) { - this._trigger("resize", event, this.ui()); - } - - return false; - }, - - _mouseStop: function(event) { - - this.resizing = false; - var pr, ista, soffseth, soffsetw, s, left, top, - o = this.options, that = this; - - if(this._helper) { - - pr = this._proportionallyResizeElements; - ista = pr.length && (/textarea/i).test(pr[0].nodeName); - soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height; - soffsetw = ista ? 0 : that.sizeDiff.width; - - s = { width: (that.helper.width() - soffsetw), height: (that.helper.height() - soffseth) }; - left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null; - top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; - - if (!o.animate) { - this.element.css($.extend(s, { top: top, left: left })); - } - - that.helper.height(that.size.height); - that.helper.width(that.size.width); - - if (this._helper && !o.animate) { - this._proportionallyResize(); - } - } - - $("body").css("cursor", "auto"); - - this.element.removeClass("ui-resizable-resizing"); - - this._propagate("stop", event); - - if (this._helper) { - this.helper.remove(); - } - - return false; - - }, - - _updateVirtualBoundaries: function(forceAspectRatio) { - var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b, - o = this.options; - - b = { - minWidth: isNumber(o.minWidth) ? o.minWidth : 0, - maxWidth: isNumber(o.maxWidth) ? o.maxWidth : Infinity, - minHeight: isNumber(o.minHeight) ? o.minHeight : 0, - maxHeight: isNumber(o.maxHeight) ? o.maxHeight : Infinity - }; - - if(this._aspectRatio || forceAspectRatio) { - // We want to create an enclosing box whose aspect ration is the requested one - // First, compute the "projected" size for each dimension based on the aspect ratio and other dimension - pMinWidth = b.minHeight * this.aspectRatio; - pMinHeight = b.minWidth / this.aspectRatio; - pMaxWidth = b.maxHeight * this.aspectRatio; - pMaxHeight = b.maxWidth / this.aspectRatio; - - if(pMinWidth > b.minWidth) { - b.minWidth = pMinWidth; - } - if(pMinHeight > b.minHeight) { - b.minHeight = pMinHeight; - } - if(pMaxWidth < b.maxWidth) { - b.maxWidth = pMaxWidth; - } - if(pMaxHeight < b.maxHeight) { - b.maxHeight = pMaxHeight; - } - } - this._vBoundaries = b; - }, - - _updateCache: function(data) { - this.offset = this.helper.offset(); - if (isNumber(data.left)) { - this.position.left = data.left; - } - if (isNumber(data.top)) { - this.position.top = data.top; - } - if (isNumber(data.height)) { - this.size.height = data.height; - } - if (isNumber(data.width)) { - this.size.width = data.width; - } - }, - - _updateRatio: function( data ) { - - var cpos = this.position, - csize = this.size, - a = this.axis; - - if (isNumber(data.height)) { - data.width = (data.height * this.aspectRatio); - } else if (isNumber(data.width)) { - data.height = (data.width / this.aspectRatio); - } - - if (a === "sw") { - data.left = cpos.left + (csize.width - data.width); - data.top = null; - } - if (a === "nw") { - data.top = cpos.top + (csize.height - data.height); - data.left = cpos.left + (csize.width - data.width); - } - - return data; - }, - - _respectSize: function( data ) { - - var o = this._vBoundaries, - a = this.axis, - ismaxw = isNumber(data.width) && o.maxWidth && (o.maxWidth < data.width), ismaxh = isNumber(data.height) && o.maxHeight && (o.maxHeight < data.height), - isminw = isNumber(data.width) && o.minWidth && (o.minWidth > data.width), isminh = isNumber(data.height) && o.minHeight && (o.minHeight > data.height), - dw = this.originalPosition.left + this.originalSize.width, - dh = this.position.top + this.size.height, - cw = /sw|nw|w/.test(a), ch = /nw|ne|n/.test(a); - if (isminw) { - data.width = o.minWidth; - } - if (isminh) { - data.height = o.minHeight; - } - if (ismaxw) { - data.width = o.maxWidth; - } - if (ismaxh) { - data.height = o.maxHeight; - } - - if (isminw && cw) { - data.left = dw - o.minWidth; - } - if (ismaxw && cw) { - data.left = dw - o.maxWidth; - } - if (isminh && ch) { - data.top = dh - o.minHeight; - } - if (ismaxh && ch) { - data.top = dh - o.maxHeight; - } - - // fixing jump error on top/left - bug #2330 - if (!data.width && !data.height && !data.left && data.top) { - data.top = null; - } else if (!data.width && !data.height && !data.top && data.left) { - data.left = null; - } - - return data; - }, - - _proportionallyResize: function() { - - if (!this._proportionallyResizeElements.length) { - return; - } - - var i, j, borders, paddings, prel, - element = this.helper || this.element; - - for ( i=0; i < this._proportionallyResizeElements.length; i++) { - - prel = this._proportionallyResizeElements[i]; - - if (!this.borderDif) { - this.borderDif = []; - borders = [prel.css("borderTopWidth"), prel.css("borderRightWidth"), prel.css("borderBottomWidth"), prel.css("borderLeftWidth")]; - paddings = [prel.css("paddingTop"), prel.css("paddingRight"), prel.css("paddingBottom"), prel.css("paddingLeft")]; - - for ( j = 0; j < borders.length; j++ ) { - this.borderDif[ j ] = ( parseInt( borders[ j ], 10 ) || 0 ) + ( parseInt( paddings[ j ], 10 ) || 0 ); - } - } - - prel.css({ - height: (element.height() - this.borderDif[0] - this.borderDif[2]) || 0, - width: (element.width() - this.borderDif[1] - this.borderDif[3]) || 0 - }); - - } - - }, - - _renderProxy: function() { - - var el = this.element, o = this.options; - this.elementOffset = el.offset(); - - if(this._helper) { - - this.helper = this.helper || $("
"); - - this.helper.addClass(this._helper).css({ - width: this.element.outerWidth() - 1, - height: this.element.outerHeight() - 1, - position: "absolute", - left: this.elementOffset.left +"px", - top: this.elementOffset.top +"px", - zIndex: ++o.zIndex //TODO: Don't modify option - }); - - this.helper - .appendTo("body") - .disableSelection(); - - } else { - this.helper = this.element; - } - - }, - - _change: { - e: function(event, dx) { - return { width: this.originalSize.width + dx }; - }, - w: function(event, dx) { - var cs = this.originalSize, sp = this.originalPosition; - return { left: sp.left + dx, width: cs.width - dx }; - }, - n: function(event, dx, dy) { - var cs = this.originalSize, sp = this.originalPosition; - return { top: sp.top + dy, height: cs.height - dy }; - }, - s: function(event, dx, dy) { - return { height: this.originalSize.height + dy }; - }, - se: function(event, dx, dy) { - return $.extend(this._change.s.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); - }, - sw: function(event, dx, dy) { - return $.extend(this._change.s.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); - }, - ne: function(event, dx, dy) { - return $.extend(this._change.n.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); - }, - nw: function(event, dx, dy) { - return $.extend(this._change.n.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); - } - }, - - _propagate: function(n, event) { - $.ui.plugin.call(this, n, [event, this.ui()]); - (n !== "resize" && this._trigger(n, event, this.ui())); - }, - - plugins: {}, - - ui: function() { - return { - originalElement: this.originalElement, - element: this.element, - helper: this.helper, - position: this.position, - size: this.size, - originalSize: this.originalSize, - originalPosition: this.originalPosition - }; - } - -}); - -/* - * Resizable Extensions - */ - -$.ui.plugin.add("resizable", "animate", { - - stop: function( event ) { - var that = $(this).data("ui-resizable"), - o = that.options, - pr = that._proportionallyResizeElements, - ista = pr.length && (/textarea/i).test(pr[0].nodeName), - soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height, - soffsetw = ista ? 0 : that.sizeDiff.width, - style = { width: (that.size.width - soffsetw), height: (that.size.height - soffseth) }, - left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null, - top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; - - that.element.animate( - $.extend(style, top && left ? { top: top, left: left } : {}), { - duration: o.animateDuration, - easing: o.animateEasing, - step: function() { - - var data = { - width: parseInt(that.element.css("width"), 10), - height: parseInt(that.element.css("height"), 10), - top: parseInt(that.element.css("top"), 10), - left: parseInt(that.element.css("left"), 10) - }; - - if (pr && pr.length) { - $(pr[0]).css({ width: data.width, height: data.height }); - } - - // propagating resize, and updating values for each animation step - that._updateCache(data); - that._propagate("resize", event); - - } - } - ); - } - -}); - -$.ui.plugin.add("resizable", "containment", { - - start: function() { - var element, p, co, ch, cw, width, height, - that = $(this).data("ui-resizable"), - o = that.options, - el = that.element, - oc = o.containment, - ce = (oc instanceof $) ? oc.get(0) : (/parent/.test(oc)) ? el.parent().get(0) : oc; - - if (!ce) { - return; - } - - that.containerElement = $(ce); - - if (/document/.test(oc) || oc === document) { - that.containerOffset = { left: 0, top: 0 }; - that.containerPosition = { left: 0, top: 0 }; - - that.parentData = { - element: $(document), left: 0, top: 0, - width: $(document).width(), height: $(document).height() || document.body.parentNode.scrollHeight - }; - } - - // i'm a node, so compute top, left, right, bottom - else { - element = $(ce); - p = []; - $([ "Top", "Right", "Left", "Bottom" ]).each(function(i, name) { p[i] = num(element.css("padding" + name)); }); - - that.containerOffset = element.offset(); - that.containerPosition = element.position(); - that.containerSize = { height: (element.innerHeight() - p[3]), width: (element.innerWidth() - p[1]) }; - - co = that.containerOffset; - ch = that.containerSize.height; - cw = that.containerSize.width; - width = ($.ui.hasScroll(ce, "left") ? ce.scrollWidth : cw ); - height = ($.ui.hasScroll(ce) ? ce.scrollHeight : ch); - - that.parentData = { - element: ce, left: co.left, top: co.top, width: width, height: height - }; - } - }, - - resize: function( event ) { - var woset, hoset, isParent, isOffsetRelative, - that = $(this).data("ui-resizable"), - o = that.options, - co = that.containerOffset, cp = that.position, - pRatio = that._aspectRatio || event.shiftKey, - cop = { top:0, left:0 }, ce = that.containerElement; - - if (ce[0] !== document && (/static/).test(ce.css("position"))) { - cop = co; - } - - if (cp.left < (that._helper ? co.left : 0)) { - that.size.width = that.size.width + (that._helper ? (that.position.left - co.left) : (that.position.left - cop.left)); - if (pRatio) { - that.size.height = that.size.width / that.aspectRatio; - } - that.position.left = o.helper ? co.left : 0; - } - - if (cp.top < (that._helper ? co.top : 0)) { - that.size.height = that.size.height + (that._helper ? (that.position.top - co.top) : that.position.top); - if (pRatio) { - that.size.width = that.size.height * that.aspectRatio; - } - that.position.top = that._helper ? co.top : 0; - } - - that.offset.left = that.parentData.left+that.position.left; - that.offset.top = that.parentData.top+that.position.top; - - woset = Math.abs( (that._helper ? that.offset.left - cop.left : (that.offset.left - cop.left)) + that.sizeDiff.width ); - hoset = Math.abs( (that._helper ? that.offset.top - cop.top : (that.offset.top - co.top)) + that.sizeDiff.height ); - - isParent = that.containerElement.get(0) === that.element.parent().get(0); - isOffsetRelative = /relative|absolute/.test(that.containerElement.css("position")); - - if(isParent && isOffsetRelative) { - woset -= that.parentData.left; - } - - if (woset + that.size.width >= that.parentData.width) { - that.size.width = that.parentData.width - woset; - if (pRatio) { - that.size.height = that.size.width / that.aspectRatio; - } - } - - if (hoset + that.size.height >= that.parentData.height) { - that.size.height = that.parentData.height - hoset; - if (pRatio) { - that.size.width = that.size.height * that.aspectRatio; - } - } - }, - - stop: function(){ - var that = $(this).data("ui-resizable"), - o = that.options, - co = that.containerOffset, - cop = that.containerPosition, - ce = that.containerElement, - helper = $(that.helper), - ho = helper.offset(), - w = helper.outerWidth() - that.sizeDiff.width, - h = helper.outerHeight() - that.sizeDiff.height; - - if (that._helper && !o.animate && (/relative/).test(ce.css("position"))) { - $(this).css({ left: ho.left - cop.left - co.left, width: w, height: h }); - } - - if (that._helper && !o.animate && (/static/).test(ce.css("position"))) { - $(this).css({ left: ho.left - cop.left - co.left, width: w, height: h }); - } - - } -}); - -$.ui.plugin.add("resizable", "alsoResize", { - - start: function () { - var that = $(this).data("ui-resizable"), - o = that.options, - _store = function (exp) { - $(exp).each(function() { - var el = $(this); - el.data("ui-resizable-alsoresize", { - width: parseInt(el.width(), 10), height: parseInt(el.height(), 10), - left: parseInt(el.css("left"), 10), top: parseInt(el.css("top"), 10) - }); - }); - }; - - if (typeof(o.alsoResize) === "object" && !o.alsoResize.parentNode) { - if (o.alsoResize.length) { o.alsoResize = o.alsoResize[0]; _store(o.alsoResize); } - else { $.each(o.alsoResize, function (exp) { _store(exp); }); } - }else{ - _store(o.alsoResize); - } - }, - - resize: function (event, ui) { - var that = $(this).data("ui-resizable"), - o = that.options, - os = that.originalSize, - op = that.originalPosition, - delta = { - height: (that.size.height - os.height) || 0, width: (that.size.width - os.width) || 0, - top: (that.position.top - op.top) || 0, left: (that.position.left - op.left) || 0 - }, - - _alsoResize = function (exp, c) { - $(exp).each(function() { - var el = $(this), start = $(this).data("ui-resizable-alsoresize"), style = {}, - css = c && c.length ? c : el.parents(ui.originalElement[0]).length ? ["width", "height"] : ["width", "height", "top", "left"]; - - $.each(css, function (i, prop) { - var sum = (start[prop]||0) + (delta[prop]||0); - if (sum && sum >= 0) { - style[prop] = sum || null; - } - }); - - el.css(style); - }); - }; - - if (typeof(o.alsoResize) === "object" && !o.alsoResize.nodeType) { - $.each(o.alsoResize, function (exp, c) { _alsoResize(exp, c); }); - }else{ - _alsoResize(o.alsoResize); - } - }, - - stop: function () { - $(this).removeData("resizable-alsoresize"); - } -}); - -$.ui.plugin.add("resizable", "ghost", { - - start: function() { - - var that = $(this).data("ui-resizable"), o = that.options, cs = that.size; - - that.ghost = that.originalElement.clone(); - that.ghost - .css({ opacity: 0.25, display: "block", position: "relative", height: cs.height, width: cs.width, margin: 0, left: 0, top: 0 }) - .addClass("ui-resizable-ghost") - .addClass(typeof o.ghost === "string" ? o.ghost : ""); - - that.ghost.appendTo(that.helper); - - }, - - resize: function(){ - var that = $(this).data("ui-resizable"); - if (that.ghost) { - that.ghost.css({ position: "relative", height: that.size.height, width: that.size.width }); - } - }, - - stop: function() { - var that = $(this).data("ui-resizable"); - if (that.ghost && that.helper) { - that.helper.get(0).removeChild(that.ghost.get(0)); - } - } - -}); - -$.ui.plugin.add("resizable", "grid", { - - resize: function() { - var that = $(this).data("ui-resizable"), - o = that.options, - cs = that.size, - os = that.originalSize, - op = that.originalPosition, - a = that.axis, - grid = typeof o.grid === "number" ? [o.grid, o.grid] : o.grid, - gridX = (grid[0]||1), - gridY = (grid[1]||1), - ox = Math.round((cs.width - os.width) / gridX) * gridX, - oy = Math.round((cs.height - os.height) / gridY) * gridY, - newWidth = os.width + ox, - newHeight = os.height + oy, - isMaxWidth = o.maxWidth && (o.maxWidth < newWidth), - isMaxHeight = o.maxHeight && (o.maxHeight < newHeight), - isMinWidth = o.minWidth && (o.minWidth > newWidth), - isMinHeight = o.minHeight && (o.minHeight > newHeight); - - o.grid = grid; - - if (isMinWidth) { - newWidth = newWidth + gridX; - } - if (isMinHeight) { - newHeight = newHeight + gridY; - } - if (isMaxWidth) { - newWidth = newWidth - gridX; - } - if (isMaxHeight) { - newHeight = newHeight - gridY; - } - - if (/^(se|s|e)$/.test(a)) { - that.size.width = newWidth; - that.size.height = newHeight; - } else if (/^(ne)$/.test(a)) { - that.size.width = newWidth; - that.size.height = newHeight; - that.position.top = op.top - oy; - } else if (/^(sw)$/.test(a)) { - that.size.width = newWidth; - that.size.height = newHeight; - that.position.left = op.left - ox; - } else { - that.size.width = newWidth; - that.size.height = newHeight; - that.position.top = op.top - oy; - that.position.left = op.left - ox; - } - } - -}); - -})(jQuery); - -(function( $, undefined ) { - -$.widget("ui.selectable", $.ui.mouse, { - version: "1.10.3", - options: { - appendTo: "body", - autoRefresh: true, - distance: 0, - filter: "*", - tolerance: "touch", - - // callbacks - selected: null, - selecting: null, - start: null, - stop: null, - unselected: null, - unselecting: null - }, - _create: function() { - var selectees, - that = this; - - this.element.addClass("ui-selectable"); - - this.dragged = false; - - // cache selectee children based on filter - this.refresh = function() { - selectees = $(that.options.filter, that.element[0]); - selectees.addClass("ui-selectee"); - selectees.each(function() { - var $this = $(this), - pos = $this.offset(); - $.data(this, "selectable-item", { - element: this, - $element: $this, - left: pos.left, - top: pos.top, - right: pos.left + $this.outerWidth(), - bottom: pos.top + $this.outerHeight(), - startselected: false, - selected: $this.hasClass("ui-selected"), - selecting: $this.hasClass("ui-selecting"), - unselecting: $this.hasClass("ui-unselecting") - }); - }); - }; - this.refresh(); - - this.selectees = selectees.addClass("ui-selectee"); - - this._mouseInit(); - - this.helper = $("
"); - }, - - _destroy: function() { - this.selectees - .removeClass("ui-selectee") - .removeData("selectable-item"); - this.element - .removeClass("ui-selectable ui-selectable-disabled"); - this._mouseDestroy(); - }, - - _mouseStart: function(event) { - var that = this, - options = this.options; - - this.opos = [event.pageX, event.pageY]; - - if (this.options.disabled) { - return; - } - - this.selectees = $(options.filter, this.element[0]); - - this._trigger("start", event); - - $(options.appendTo).append(this.helper); - // position helper (lasso) - this.helper.css({ - "left": event.pageX, - "top": event.pageY, - "width": 0, - "height": 0 - }); - - if (options.autoRefresh) { - this.refresh(); - } - - this.selectees.filter(".ui-selected").each(function() { - var selectee = $.data(this, "selectable-item"); - selectee.startselected = true; - if (!event.metaKey && !event.ctrlKey) { - selectee.$element.removeClass("ui-selected"); - selectee.selected = false; - selectee.$element.addClass("ui-unselecting"); - selectee.unselecting = true; - // selectable UNSELECTING callback - that._trigger("unselecting", event, { - unselecting: selectee.element - }); - } - }); - - $(event.target).parents().addBack().each(function() { - var doSelect, - selectee = $.data(this, "selectable-item"); - if (selectee) { - doSelect = (!event.metaKey && !event.ctrlKey) || !selectee.$element.hasClass("ui-selected"); - selectee.$element - .removeClass(doSelect ? "ui-unselecting" : "ui-selected") - .addClass(doSelect ? "ui-selecting" : "ui-unselecting"); - selectee.unselecting = !doSelect; - selectee.selecting = doSelect; - selectee.selected = doSelect; - // selectable (UN)SELECTING callback - if (doSelect) { - that._trigger("selecting", event, { - selecting: selectee.element - }); - } else { - that._trigger("unselecting", event, { - unselecting: selectee.element - }); - } - return false; - } - }); - - }, - - _mouseDrag: function(event) { - - this.dragged = true; - - if (this.options.disabled) { - return; - } - - var tmp, - that = this, - options = this.options, - x1 = this.opos[0], - y1 = this.opos[1], - x2 = event.pageX, - y2 = event.pageY; - - if (x1 > x2) { tmp = x2; x2 = x1; x1 = tmp; } - if (y1 > y2) { tmp = y2; y2 = y1; y1 = tmp; } - this.helper.css({left: x1, top: y1, width: x2-x1, height: y2-y1}); - - this.selectees.each(function() { - var selectee = $.data(this, "selectable-item"), - hit = false; - - //prevent helper from being selected if appendTo: selectable - if (!selectee || selectee.element === that.element[0]) { - return; - } - - if (options.tolerance === "touch") { - hit = ( !(selectee.left > x2 || selectee.right < x1 || selectee.top > y2 || selectee.bottom < y1) ); - } else if (options.tolerance === "fit") { - hit = (selectee.left > x1 && selectee.right < x2 && selectee.top > y1 && selectee.bottom < y2); - } - - if (hit) { - // SELECT - if (selectee.selected) { - selectee.$element.removeClass("ui-selected"); - selectee.selected = false; - } - if (selectee.unselecting) { - selectee.$element.removeClass("ui-unselecting"); - selectee.unselecting = false; - } - if (!selectee.selecting) { - selectee.$element.addClass("ui-selecting"); - selectee.selecting = true; - // selectable SELECTING callback - that._trigger("selecting", event, { - selecting: selectee.element - }); - } - } else { - // UNSELECT - if (selectee.selecting) { - if ((event.metaKey || event.ctrlKey) && selectee.startselected) { - selectee.$element.removeClass("ui-selecting"); - selectee.selecting = false; - selectee.$element.addClass("ui-selected"); - selectee.selected = true; - } else { - selectee.$element.removeClass("ui-selecting"); - selectee.selecting = false; - if (selectee.startselected) { - selectee.$element.addClass("ui-unselecting"); - selectee.unselecting = true; - } - // selectable UNSELECTING callback - that._trigger("unselecting", event, { - unselecting: selectee.element - }); - } - } - if (selectee.selected) { - if (!event.metaKey && !event.ctrlKey && !selectee.startselected) { - selectee.$element.removeClass("ui-selected"); - selectee.selected = false; - - selectee.$element.addClass("ui-unselecting"); - selectee.unselecting = true; - // selectable UNSELECTING callback - that._trigger("unselecting", event, { - unselecting: selectee.element - }); - } - } - } - }); - - return false; - }, - - _mouseStop: function(event) { - var that = this; - - this.dragged = false; - - $(".ui-unselecting", this.element[0]).each(function() { - var selectee = $.data(this, "selectable-item"); - selectee.$element.removeClass("ui-unselecting"); - selectee.unselecting = false; - selectee.startselected = false; - that._trigger("unselected", event, { - unselected: selectee.element - }); - }); - $(".ui-selecting", this.element[0]).each(function() { - var selectee = $.data(this, "selectable-item"); - selectee.$element.removeClass("ui-selecting").addClass("ui-selected"); - selectee.selecting = false; - selectee.selected = true; - selectee.startselected = true; - that._trigger("selected", event, { - selected: selectee.element - }); - }); - this._trigger("stop", event); - - this.helper.remove(); - - return false; - } - -}); - -})(jQuery); - -(function( $, undefined ) { - -/*jshint loopfunc: true */ - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -function isFloating(item) { - return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); -} - -$.widget("ui.sortable", $.ui.mouse, { - version: "1.10.3", - widgetEventPrefix: "sort", - ready: false, - options: { - appendTo: "parent", - axis: false, - connectWith: false, - containment: false, - cursor: "auto", - cursorAt: false, - dropOnEmpty: true, - forcePlaceholderSize: false, - forceHelperSize: false, - grid: false, - handle: false, - helper: "original", - items: "> *", - opacity: false, - placeholder: false, - revert: false, - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - scope: "default", - tolerance: "intersect", - zIndex: 1000, - - // callbacks - activate: null, - beforeStop: null, - change: null, - deactivate: null, - out: null, - over: null, - receive: null, - remove: null, - sort: null, - start: null, - stop: null, - update: null - }, - _create: function() { - - var o = this.options; - this.containerCache = {}; - this.element.addClass("ui-sortable"); - - //Get the items - this.refresh(); - - //Let's determine if the items are being displayed horizontally - this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; - - //Let's determine the parent's offset - this.offset = this.element.offset(); - - //Initialize mouse events for interaction - this._mouseInit(); - - //We're ready to go - this.ready = true; - - }, - - _destroy: function() { - this.element - .removeClass("ui-sortable ui-sortable-disabled"); - this._mouseDestroy(); - - for ( var i = this.items.length - 1; i >= 0; i-- ) { - this.items[i].item.removeData(this.widgetName + "-item"); - } - - return this; - }, - - _setOption: function(key, value){ - if ( key === "disabled" ) { - this.options[ key ] = value; - - this.widget().toggleClass( "ui-sortable-disabled", !!value ); - } else { - // Don't call widget base _setOption for disable as it adds ui-state-disabled class - $.Widget.prototype._setOption.apply(this, arguments); - } - }, - - _mouseCapture: function(event, overrideHandle) { - var currentItem = null, - validHandle = false, - that = this; - - if (this.reverting) { - return false; - } - - if(this.options.disabled || this.options.type === "static") { - return false; - } - - //We have to refresh the items data once first - this._refreshItems(event); - - //Find out if the clicked node (or one of its parents) is a actual item in this.items - $(event.target).parents().each(function() { - if($.data(this, that.widgetName + "-item") === that) { - currentItem = $(this); - return false; - } - }); - if($.data(event.target, that.widgetName + "-item") === that) { - currentItem = $(event.target); - } - - if(!currentItem) { - return false; - } - if(this.options.handle && !overrideHandle) { - $(this.options.handle, currentItem).find("*").addBack().each(function() { - if(this === event.target) { - validHandle = true; - } - }); - if(!validHandle) { - return false; - } - } - - this.currentItem = currentItem; - this._removeCurrentsFromItems(); - return true; - - }, - - _mouseStart: function(event, overrideHandle, noActivation) { - - var i, body, - o = this.options; - - this.currentContainer = this; - - //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture - this.refreshPositions(); - - //Create and append the visible helper - this.helper = this._createHelper(event); - - //Cache the helper size - this._cacheHelperProportions(); - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Get the next scrolling parent - this.scrollParent = this.helper.scrollParent(); - - //The element's absolute position on the page minus margins - this.offset = this.currentItem.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - // Only after we got the offset, we can change the helper's position to absolute - // TODO: Still need to figure out a way to make relative sorting possible - this.helper.css("position", "absolute"); - this.cssPosition = this.helper.css("position"); - - //Generate the original position - this.originalPosition = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Cache the former DOM position - this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; - - //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way - if(this.helper[0] !== this.currentItem[0]) { - this.currentItem.hide(); - } - - //Create the placeholder - this._createPlaceholder(); - - //Set a containment if given in the options - if(o.containment) { - this._setContainment(); - } - - if( o.cursor && o.cursor !== "auto" ) { // cursor option - body = this.document.find( "body" ); - - // support: IE - this.storedCursor = body.css( "cursor" ); - body.css( "cursor", o.cursor ); - - this.storedStylesheet = $( "" ).appendTo( body ); - } - - if(o.opacity) { // opacity option - if (this.helper.css("opacity")) { - this._storedOpacity = this.helper.css("opacity"); - } - this.helper.css("opacity", o.opacity); - } - - if(o.zIndex) { // zIndex option - if (this.helper.css("zIndex")) { - this._storedZIndex = this.helper.css("zIndex"); - } - this.helper.css("zIndex", o.zIndex); - } - - //Prepare scrolling - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - this.overflowOffset = this.scrollParent.offset(); - } - - //Call callbacks - this._trigger("start", event, this._uiHash()); - - //Recache the helper size - if(!this._preserveHelperProportions) { - this._cacheHelperProportions(); - } - - - //Post "activate" events to possible containers - if( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } - - //Prepare possible droppables - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - this.dragging = true; - - this.helper.addClass("ui-sortable-helper"); - this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position - return true; - - }, - - _mouseDrag: function(event) { - var i, item, itemElement, intersection, - o = this.options, - scrolled = false; - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - if (!this.lastPositionAbs) { - this.lastPositionAbs = this.positionAbs; - } - - //Do scrolling - if(this.options.scroll) { - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - - if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; - } - - if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; - } - - } else { - - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - } - - //Regenerate the absolute position used for position checks - this.positionAbs = this._convertPositionTo("absolute"); - - //Set the helper position - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - - //Rearrange - for (i = this.items.length - 1; i >= 0; i--) { - - //Cache variables and intersection, continue if no intersection - item = this.items[i]; - itemElement = item.item[0]; - intersection = this._intersectsWithPointer(item); - if (!intersection) { - continue; - } - - // Only put the placeholder inside the current Container, skip all - // items form other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this moving items in "sub-sortables" can cause the placeholder to jitter - // beetween the outer and inner container. - if (item.instance !== this.currentContainer) { - continue; - } - - // cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if (itemElement !== this.currentItem[0] && - this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && - !$.contains(this.placeholder[0], itemElement) && - (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) - ) { - - this.direction = intersection === 1 ? "down" : "up"; - - if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { - this._rearrange(event, item); - } else { - break; - } - - this._trigger("change", event, this._uiHash()); - break; - } - } - - //Post events to containers - this._contactContainers(event); - - //Interconnect with droppables - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - //Call callbacks - this._trigger("sort", event, this._uiHash()); - - this.lastPositionAbs = this.positionAbs; - return false; - - }, - - _mouseStop: function(event, noPropagation) { - - if(!event) { - return; - } - - //If we are using droppables, inform the manager about the drop - if ($.ui.ddmanager && !this.options.dropBehaviour) { - $.ui.ddmanager.drop(this, event); - } - - if(this.options.revert) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; - - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); - } - this.reverting = true; - $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { - that._clear(event); - }); - } else { - this._clear(event, noPropagation); - } - - return false; - - }, - - cancel: function() { - - if(this.dragging) { - - this._mouseUp({ target: null }); - - if(this.options.helper === "original") { - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - //Post deactivating events to containers - for (var i = this.containers.length - 1; i >= 0; i--){ - this.containers[i]._trigger("deactivate", null, this._uiHash(this)); - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", null, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - if (this.placeholder) { - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - if(this.placeholder[0].parentNode) { - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - } - if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { - this.helper.remove(); - } - - $.extend(this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - }); - - if(this.domPosition.prev) { - $(this.domPosition.prev).after(this.currentItem); - } else { - $(this.domPosition.parent).prepend(this.currentItem); - } - } - - return this; - - }, - - serialize: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - str = []; - o = o || {}; - - $(items).each(function() { - var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); - if (res) { - str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); - } - }); - - if(!str.length && o.key) { - str.push(o.key + "="); - } - - return str.join("&"); - - }, - - toArray: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - ret = []; - - o = o || {}; - - items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); - return ret; - - }, - - /* Be careful with the following core functions */ - _intersectsWith: function(item) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) - ) { - return isOverElement; - } else { - - return (l < x1 + (this.helperProportions.width / 2) && // Right Half - x2 - (this.helperProportions.width / 2) < r && // Left Half - t < y1 + (this.helperProportions.height / 2) && // Bottom Half - y2 - (this.helperProportions.height / 2) < b ); // Top Half - - } - }, - - _intersectsWithPointer: function(item) { - - var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), - isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), - isOverElement = isOverElementHeight && isOverElementWidth, - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (!isOverElement) { - return false; - } - - return this.floating ? - ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) - : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); - - }, - - _intersectsWithSides: function(item) { - - var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), - isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (this.floating && horizontalDirection) { - return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); - } else { - return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); - } - - }, - - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && (delta > 0 ? "down" : "up"); - }, - - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && (delta > 0 ? "right" : "left"); - }, - - refresh: function(event) { - this._refreshItems(event); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; - }, - - _getItemsAsjQuery: function(connected) { - - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); - - if(connectWith && connected) { - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for ( j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); - } - } - } - } - - queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); - - for (i = queries.length - 1; i >= 0; i--){ - queries[i][0].each(function() { - items.push(this); - }); - } - - return $(items); - - }, - - _removeCurrentsFromItems: function() { - - var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); - - this.items = $.grep(this.items, function (item) { - for (var j=0; j < list.length; j++) { - if(list[j] === item.item[0]) { - return false; - } - } - return true; - }); - - }, - - _refreshItems: function(event) { - - this.items = []; - this.containers = [this]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], - connectWith = this._connectWith(); - - if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for (j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); - this.containers.push(inst); - } - } - } - } - - for (i = queries.length - 1; i >= 0; i--) { - targetData = queries[i][1]; - _queries = queries[i][0]; - - for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { - item = $(_queries[j]); - - item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) - - items.push({ - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - }); - } - } - - }, - - refreshPositions: function(fast) { - - //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change - if(this.offsetParent && this.helper) { - this.offset.parent = this._getParentOffset(); - } - - var i, item, t, p; - - for (i = this.items.length - 1; i >= 0; i--){ - item = this.items[i]; - - //We ignore calculating positions of all connected containers when we're not over them - if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { - continue; - } - - t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; - - if (!fast) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } - - p = t.offset(); - item.left = p.left; - item.top = p.top; - } - - if(this.options.custom && this.options.custom.refreshContainers) { - this.options.custom.refreshContainers.call(this); - } else { - for (i = this.containers.length - 1; i >= 0; i--){ - p = this.containers[i].element.offset(); - this.containers[i].containerCache.left = p.left; - this.containers[i].containerCache.top = p.top; - this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); - this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); - } - } - - return this; - }, - - _createPlaceholder: function(that) { - that = that || this; - var className, - o = that.options; - - if(!o.placeholder || o.placeholder.constructor === String) { - className = o.placeholder; - o.placeholder = { - element: function() { - - var nodeName = that.currentItem[0].nodeName.toLowerCase(), - element = $( "<" + nodeName + ">", that.document[0] ) - .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") - .removeClass("ui-sortable-helper"); - - if ( nodeName === "tr" ) { - that.currentItem.children().each(function() { - $( " ", that.document[0] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( element ); - }); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } - - if ( !className ) { - element.css( "visibility", "hidden" ); - } - - return element; - }, - update: function(container, p) { - - // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified - if(className && !o.forcePlaceholderSize) { - return; - } - - //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item - if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } - if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } - } - }; - } - - //Create the placeholder - that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); - - //Append it after the actual current item - that.currentItem.after(that.placeholder); - - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update(that, that.placeholder); - - }, - - _contactContainers: function(event) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, - innermostContainer = null, - innermostIndex = null; - - // get innermost container that intersects with item - for (i = this.containers.length - 1; i >= 0; i--) { - - // never consider a container that's located within the item itself - if($.contains(this.currentItem[0], this.containers[i].element[0])) { - continue; - } - - if(this._intersectsWith(this.containers[i].containerCache)) { - - // if we've already found a container and it's more "inner" than this, then continue - if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { - continue; - } - - innermostContainer = this.containers[i]; - innermostIndex = i; - - } else { - // container doesn't intersect. trigger "out" event if necessary - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", event, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - // if no intersecting containers found, return - if(!innermostContainer) { - return; - } - - // move the item into the container if it's not there already - if(this.containers.length === 1) { - if (!this.containers[innermostIndex].containerCache.over) { - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - } else { - - //When entering a new container, we will find the item with the least distance and append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || isFloating(this.currentItem); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - base = this.positionAbs[posProperty] + this.offset.click[posProperty]; - for (j = this.items.length - 1; j >= 0; j--) { - if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { - continue; - } - if(this.items[j].item[0] === this.currentItem[0]) { - continue; - } - if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { - continue; - } - cur = this.items[j].item.offset()[posProperty]; - nearBottom = false; - if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ - nearBottom = true; - cur += this.items[j][sizeProperty]; - } - - if(Math.abs(cur - base) < dist) { - dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; - this.direction = nearBottom ? "up": "down"; - } - } - - //Check if dropOnEmpty is enabled - if(!itemWithLeastDistance && !this.options.dropOnEmpty) { - return; - } - - if(this.currentContainer === this.containers[innermostIndex]) { - return; - } - - itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); - this._trigger("change", event, this._uiHash()); - this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); - this.currentContainer = this.containers[innermostIndex]; - - //Update the placeholder - this.options.placeholder.update(this.currentContainer, this.placeholder); - - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - - - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); - - //Add the helper to the DOM if that didn't happen already - if(!helper.parents("body").length) { - $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); - } - - if(helper[0] === this.currentItem[0]) { - this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; - } - - if(!helper[0].style.width || o.forceHelperSize) { - helper.width(this.currentItem.width()); - } - if(!helper[0].style.height || o.forceHelperSize) { - helper.height(this.currentItem.height()); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - // This needs to be actually done for all browsers, since pageX/pageY includes this information - // with an ugly IE fix - if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.currentItem.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), - top: (parseInt(this.currentItem.css("marginTop"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var ce, co, over, - o = this.options; - if(o.containment === "parent") { - o.containment = this.helper[0].parentNode; - } - if(o.containment === "document" || o.containment === "window") { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, - ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top - ]; - } - - if(!(/^(document|window|parent)$/).test(o.containment)) { - ce = $(o.containment)[0]; - co = $(o.containment).offset(); - over = ($(ce).css("overflow") !== "hidden"); - - this.containment = [ - co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, - co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, - co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, - co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top - ]; - } - - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, - scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) - ) - }; - - }, - - _generatePosition: function(event) { - - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { - this.offset.relative = this._getRelativeOffset(); - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - if(this.originalPosition) { //If we are not dragging yet, we won't check for options - - if(this.containment) { - if(event.pageX - this.offset.click.left < this.containment[0]) { - pageX = this.containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < this.containment[1]) { - pageY = this.containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > this.containment[2]) { - pageX = this.containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > this.containment[3]) { - pageY = this.containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; - pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; - pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) - ) - }; - - }, - - _rearrange: function(event, i, a, hardRefresh) { - - a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); - - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay(function() { - if(counter === this.counter) { - this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove - } - }); - - }, - - _clear: function(event, noPropagation) { - - this.reverting = false; - // We delay all events that have to be triggered to after the point where the placeholder has been removed and - // everything else normalized again - var i, - delayedTriggers = []; - - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) - if(!this._noFinalSort && this.currentItem.parent().length) { - this.placeholder.before(this.currentItem); - } - this._noFinalSort = null; - - if(this.helper[0] === this.currentItem[0]) { - for(i in this._storedCSS) { - if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { - this._storedCSS[i] = ""; - } - } - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - if(this.fromOutside && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); - } - if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed - } - - // Check if the items Container has Changed and trigger appropriate - // events. - if (this !== this.currentContainer) { - if(!noPropagation) { - delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); - delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - } - } - - - //Post events to containers - for (i = this.containers.length - 1; i >= 0; i--){ - if(!noPropagation) { - delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); }; }).call(this, this.containers[i])); - } - if(this.containers[i].containerCache.over) { - delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); }; }).call(this, this.containers[i])); - this.containers[i].containerCache.over = 0; - } - } - - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if(this._storedOpacity) { - this.helper.css("opacity", this._storedOpacity); - } - if(this._storedZIndex) { - this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); - } - - this.dragging = false; - if(this.cancelHelperRemoval) { - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return false; - } - - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - - if(this.helper[0] !== this.currentItem[0]) { - this.helper.remove(); - } - this.helper = null; - - if(!noPropagation) { - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return true; - - }, - - _trigger: function() { - if ($.Widget.prototype._trigger.apply(this, arguments) === false) { - this.cancel(); - } - }, - - _uiHash: function(_inst) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $([]), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; - } - -}); - -})(jQuery); - -(function($, undefined) { - -var dataSpace = "ui-effects-"; - -$.effects = { - effect: {} -}; - -/*! - * jQuery Color Animations v2.1.2 - * https://github.com/jquery/jquery-color - * - * Copyright 2013 jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * Date: Wed Jan 16 08:47:09 2013 -0600 - */ -(function( jQuery, undefined ) { - - var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", - - // plusequals test for += 100 -= 100 - rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, - // a set of RE's that can match strings and generate color tuples. - stringParsers = [{ - re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - execResult[ 1 ], - execResult[ 2 ], - execResult[ 3 ], - execResult[ 4 ] - ]; - } - }, { - re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - execResult[ 1 ] * 2.55, - execResult[ 2 ] * 2.55, - execResult[ 3 ] * 2.55, - execResult[ 4 ] - ]; - } - }, { - // this regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ], 16 ) - ]; - } - }, { - // this regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) - ]; - } - }, { - re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - space: "hsla", - parse: function( execResult ) { - return [ - execResult[ 1 ], - execResult[ 2 ] / 100, - execResult[ 3 ] / 100, - execResult[ 4 ] - ]; - } - }], - - // jQuery.Color( ) - color = jQuery.Color = function( color, green, blue, alpha ) { - return new jQuery.Color.fn.parse( color, green, blue, alpha ); - }, - spaces = { - rgba: { - props: { - red: { - idx: 0, - type: "byte" - }, - green: { - idx: 1, - type: "byte" - }, - blue: { - idx: 2, - type: "byte" - } - } - }, - - hsla: { - props: { - hue: { - idx: 0, - type: "degrees" - }, - saturation: { - idx: 1, - type: "percent" - }, - lightness: { - idx: 2, - type: "percent" - } - } - } - }, - propTypes = { - "byte": { - floor: true, - max: 255 - }, - "percent": { - max: 1 - }, - "degrees": { - mod: 360, - floor: true - } - }, - support = color.support = {}, - - // element for support tests - supportElem = jQuery( "

" )[ 0 ], - - // colors = jQuery.Color.names - colors, - - // local aliases of functions called often - each = jQuery.each; - -// determine rgba support immediately -supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; -support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; - -// define cache name and alpha properties -// for rgba and hsla spaces -each( spaces, function( spaceName, space ) { - space.cache = "_" + spaceName; - space.props.alpha = { - idx: 3, - type: "percent", - def: 1 - }; -}); - -function clamp( value, prop, allowEmpty ) { - var type = propTypes[ prop.type ] || {}; - - if ( value == null ) { - return (allowEmpty || !prop.def) ? null : prop.def; - } - - // ~~ is an short way of doing floor for positive numbers - value = type.floor ? ~~value : parseFloat( value ); - - // IE will pass in empty strings as value for alpha, - // which will hit this case - if ( isNaN( value ) ) { - return prop.def; - } - - if ( type.mod ) { - // we add mod before modding to make sure that negatives values - // get converted properly: -10 -> 350 - return (value + type.mod) % type.mod; - } - - // for now all property types without mod have min and max - return 0 > value ? 0 : type.max < value ? type.max : value; -} - -function stringParse( string ) { - var inst = color(), - rgba = inst._rgba = []; - - string = string.toLowerCase(); - - each( stringParsers, function( i, parser ) { - var parsed, - match = parser.re.exec( string ), - values = match && parser.parse( match ), - spaceName = parser.space || "rgba"; - - if ( values ) { - parsed = inst[ spaceName ]( values ); - - // if this was an rgba parse the assignment might happen twice - // oh well.... - inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; - rgba = inst._rgba = parsed._rgba; - - // exit each( stringParsers ) here because we matched - return false; - } - }); - - // Found a stringParser that handled it - if ( rgba.length ) { - - // if this came from a parsed string, force "transparent" when alpha is 0 - // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) - if ( rgba.join() === "0,0,0,0" ) { - jQuery.extend( rgba, colors.transparent ); - } - return inst; - } - - // named colors - return colors[ string ]; -} - -color.fn = jQuery.extend( color.prototype, { - parse: function( red, green, blue, alpha ) { - if ( red === undefined ) { - this._rgba = [ null, null, null, null ]; - return this; - } - if ( red.jquery || red.nodeType ) { - red = jQuery( red ).css( green ); - green = undefined; - } - - var inst = this, - type = jQuery.type( red ), - rgba = this._rgba = []; - - // more than 1 argument specified - assume ( red, green, blue, alpha ) - if ( green !== undefined ) { - red = [ red, green, blue, alpha ]; - type = "array"; - } - - if ( type === "string" ) { - return this.parse( stringParse( red ) || colors._default ); - } - - if ( type === "array" ) { - each( spaces.rgba.props, function( key, prop ) { - rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); - }); - return this; - } - - if ( type === "object" ) { - if ( red instanceof color ) { - each( spaces, function( spaceName, space ) { - if ( red[ space.cache ] ) { - inst[ space.cache ] = red[ space.cache ].slice(); - } - }); - } else { - each( spaces, function( spaceName, space ) { - var cache = space.cache; - each( space.props, function( key, prop ) { - - // if the cache doesn't exist, and we know how to convert - if ( !inst[ cache ] && space.to ) { - - // if the value was null, we don't need to copy it - // if the key was alpha, we don't need to copy it either - if ( key === "alpha" || red[ key ] == null ) { - return; - } - inst[ cache ] = space.to( inst._rgba ); - } - - // this is the only case where we allow nulls for ALL properties. - // call clamp with alwaysAllowEmpty - inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); - }); - - // everything defined but alpha? - if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { - // use the default of 1 - inst[ cache ][ 3 ] = 1; - if ( space.from ) { - inst._rgba = space.from( inst[ cache ] ); - } - } - }); - } - return this; - } - }, - is: function( compare ) { - var is = color( compare ), - same = true, - inst = this; - - each( spaces, function( _, space ) { - var localCache, - isCache = is[ space.cache ]; - if (isCache) { - localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; - each( space.props, function( _, prop ) { - if ( isCache[ prop.idx ] != null ) { - same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); - return same; - } - }); - } - return same; - }); - return same; - }, - _space: function() { - var used = [], - inst = this; - each( spaces, function( spaceName, space ) { - if ( inst[ space.cache ] ) { - used.push( spaceName ); - } - }); - return used.pop(); - }, - transition: function( other, distance ) { - var end = color( other ), - spaceName = end._space(), - space = spaces[ spaceName ], - startColor = this.alpha() === 0 ? color( "transparent" ) : this, - start = startColor[ space.cache ] || space.to( startColor._rgba ), - result = start.slice(); - - end = end[ space.cache ]; - each( space.props, function( key, prop ) { - var index = prop.idx, - startValue = start[ index ], - endValue = end[ index ], - type = propTypes[ prop.type ] || {}; - - // if null, don't override start value - if ( endValue === null ) { - return; - } - // if null - use end - if ( startValue === null ) { - result[ index ] = endValue; - } else { - if ( type.mod ) { - if ( endValue - startValue > type.mod / 2 ) { - startValue += type.mod; - } else if ( startValue - endValue > type.mod / 2 ) { - startValue -= type.mod; - } - } - result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); - } - }); - return this[ spaceName ]( result ); - }, - blend: function( opaque ) { - // if we are already opaque - return ourself - if ( this._rgba[ 3 ] === 1 ) { - return this; - } - - var rgb = this._rgba.slice(), - a = rgb.pop(), - blend = color( opaque )._rgba; - - return color( jQuery.map( rgb, function( v, i ) { - return ( 1 - a ) * blend[ i ] + a * v; - })); - }, - toRgbaString: function() { - var prefix = "rgba(", - rgba = jQuery.map( this._rgba, function( v, i ) { - return v == null ? ( i > 2 ? 1 : 0 ) : v; - }); - - if ( rgba[ 3 ] === 1 ) { - rgba.pop(); - prefix = "rgb("; - } - - return prefix + rgba.join() + ")"; - }, - toHslaString: function() { - var prefix = "hsla(", - hsla = jQuery.map( this.hsla(), function( v, i ) { - if ( v == null ) { - v = i > 2 ? 1 : 0; - } - - // catch 1 and 2 - if ( i && i < 3 ) { - v = Math.round( v * 100 ) + "%"; - } - return v; - }); - - if ( hsla[ 3 ] === 1 ) { - hsla.pop(); - prefix = "hsl("; - } - return prefix + hsla.join() + ")"; - }, - toHexString: function( includeAlpha ) { - var rgba = this._rgba.slice(), - alpha = rgba.pop(); - - if ( includeAlpha ) { - rgba.push( ~~( alpha * 255 ) ); - } - - return "#" + jQuery.map( rgba, function( v ) { - - // default to 0 when nulls exist - v = ( v || 0 ).toString( 16 ); - return v.length === 1 ? "0" + v : v; - }).join(""); - }, - toString: function() { - return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); - } -}); -color.fn.parse.prototype = color.fn; - -// hsla conversions adapted from: -// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 - -function hue2rgb( p, q, h ) { - h = ( h + 1 ) % 1; - if ( h * 6 < 1 ) { - return p + (q - p) * h * 6; - } - if ( h * 2 < 1) { - return q; - } - if ( h * 3 < 2 ) { - return p + (q - p) * ((2/3) - h) * 6; - } - return p; -} - -spaces.hsla.to = function ( rgba ) { - if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { - return [ null, null, null, rgba[ 3 ] ]; - } - var r = rgba[ 0 ] / 255, - g = rgba[ 1 ] / 255, - b = rgba[ 2 ] / 255, - a = rgba[ 3 ], - max = Math.max( r, g, b ), - min = Math.min( r, g, b ), - diff = max - min, - add = max + min, - l = add * 0.5, - h, s; - - if ( min === max ) { - h = 0; - } else if ( r === max ) { - h = ( 60 * ( g - b ) / diff ) + 360; - } else if ( g === max ) { - h = ( 60 * ( b - r ) / diff ) + 120; - } else { - h = ( 60 * ( r - g ) / diff ) + 240; - } - - // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% - // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) - if ( diff === 0 ) { - s = 0; - } else if ( l <= 0.5 ) { - s = diff / add; - } else { - s = diff / ( 2 - add ); - } - return [ Math.round(h) % 360, s, l, a == null ? 1 : a ]; -}; - -spaces.hsla.from = function ( hsla ) { - if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { - return [ null, null, null, hsla[ 3 ] ]; - } - var h = hsla[ 0 ] / 360, - s = hsla[ 1 ], - l = hsla[ 2 ], - a = hsla[ 3 ], - q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, - p = 2 * l - q; - - return [ - Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), - Math.round( hue2rgb( p, q, h ) * 255 ), - Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), - a - ]; -}; - - -each( spaces, function( spaceName, space ) { - var props = space.props, - cache = space.cache, - to = space.to, - from = space.from; - - // makes rgba() and hsla() - color.fn[ spaceName ] = function( value ) { - - // generate a cache for this space if it doesn't exist - if ( to && !this[ cache ] ) { - this[ cache ] = to( this._rgba ); - } - if ( value === undefined ) { - return this[ cache ].slice(); - } - - var ret, - type = jQuery.type( value ), - arr = ( type === "array" || type === "object" ) ? value : arguments, - local = this[ cache ].slice(); - - each( props, function( key, prop ) { - var val = arr[ type === "object" ? key : prop.idx ]; - if ( val == null ) { - val = local[ prop.idx ]; - } - local[ prop.idx ] = clamp( val, prop ); - }); - - if ( from ) { - ret = color( from( local ) ); - ret[ cache ] = local; - return ret; - } else { - return color( local ); - } - }; - - // makes red() green() blue() alpha() hue() saturation() lightness() - each( props, function( key, prop ) { - // alpha is included in more than one space - if ( color.fn[ key ] ) { - return; - } - color.fn[ key ] = function( value ) { - var vtype = jQuery.type( value ), - fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ), - local = this[ fn ](), - cur = local[ prop.idx ], - match; - - if ( vtype === "undefined" ) { - return cur; - } - - if ( vtype === "function" ) { - value = value.call( this, cur ); - vtype = jQuery.type( value ); - } - if ( value == null && prop.empty ) { - return this; - } - if ( vtype === "string" ) { - match = rplusequals.exec( value ); - if ( match ) { - value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); - } - } - local[ prop.idx ] = value; - return this[ fn ]( local ); - }; - }); -}); - -// add cssHook and .fx.step function for each named hook. -// accept a space separated string of properties -color.hook = function( hook ) { - var hooks = hook.split( " " ); - each( hooks, function( i, hook ) { - jQuery.cssHooks[ hook ] = { - set: function( elem, value ) { - var parsed, curElem, - backgroundColor = ""; - - if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { - value = color( parsed || value ); - if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { - curElem = hook === "backgroundColor" ? elem.parentNode : elem; - while ( - (backgroundColor === "" || backgroundColor === "transparent") && - curElem && curElem.style - ) { - try { - backgroundColor = jQuery.css( curElem, "backgroundColor" ); - curElem = curElem.parentNode; - } catch ( e ) { - } - } - - value = value.blend( backgroundColor && backgroundColor !== "transparent" ? - backgroundColor : - "_default" ); - } - - value = value.toRgbaString(); - } - try { - elem.style[ hook ] = value; - } catch( e ) { - // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' - } - } - }; - jQuery.fx.step[ hook ] = function( fx ) { - if ( !fx.colorInit ) { - fx.start = color( fx.elem, hook ); - fx.end = color( fx.end ); - fx.colorInit = true; - } - jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); - }; - }); - -}; - -color.hook( stepHooks ); - -jQuery.cssHooks.borderColor = { - expand: function( value ) { - var expanded = {}; - - each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) { - expanded[ "border" + part + "Color" ] = value; - }); - return expanded; - } -}; - -// Basic color names only. -// Usage of any of the other color names requires adding yourself or including -// jquery.color.svg-names.js. -colors = jQuery.Color.names = { - // 4.1. Basic color keywords - aqua: "#00ffff", - black: "#000000", - blue: "#0000ff", - fuchsia: "#ff00ff", - gray: "#808080", - green: "#008000", - lime: "#00ff00", - maroon: "#800000", - navy: "#000080", - olive: "#808000", - purple: "#800080", - red: "#ff0000", - silver: "#c0c0c0", - teal: "#008080", - white: "#ffffff", - yellow: "#ffff00", - - // 4.2.3. "transparent" color keyword - transparent: [ null, null, null, 0 ], - - _default: "#ffffff" -}; - -})( jQuery ); - - -/******************************************************************************/ -/****************************** CLASS ANIMATIONS ******************************/ -/******************************************************************************/ -(function() { - -var classAnimationActions = [ "add", "remove", "toggle" ], - shorthandStyles = { - border: 1, - borderBottom: 1, - borderColor: 1, - borderLeft: 1, - borderRight: 1, - borderTop: 1, - borderWidth: 1, - margin: 1, - padding: 1 - }; - -$.each([ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], function( _, prop ) { - $.fx.step[ prop ] = function( fx ) { - if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) { - jQuery.style( fx.elem, prop, fx.end ); - fx.setAttr = true; - } - }; -}); - -function getElementStyles( elem ) { - var key, len, - style = elem.ownerDocument.defaultView ? - elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : - elem.currentStyle, - styles = {}; - - if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { - len = style.length; - while ( len-- ) { - key = style[ len ]; - if ( typeof style[ key ] === "string" ) { - styles[ $.camelCase( key ) ] = style[ key ]; - } - } - // support: Opera, IE <9 - } else { - for ( key in style ) { - if ( typeof style[ key ] === "string" ) { - styles[ key ] = style[ key ]; - } - } - } - - return styles; -} - - -function styleDifference( oldStyle, newStyle ) { - var diff = {}, - name, value; - - for ( name in newStyle ) { - value = newStyle[ name ]; - if ( oldStyle[ name ] !== value ) { - if ( !shorthandStyles[ name ] ) { - if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) { - diff[ name ] = value; - } - } - } - } - - return diff; -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -$.effects.animateClass = function( value, duration, easing, callback ) { - var o = $.speed( duration, easing, callback ); - - return this.queue( function() { - var animated = $( this ), - baseClass = animated.attr( "class" ) || "", - applyClassChange, - allAnimations = o.children ? animated.find( "*" ).addBack() : animated; - - // map the animated objects to store the original styles. - allAnimations = allAnimations.map(function() { - var el = $( this ); - return { - el: el, - start: getElementStyles( this ) - }; - }); - - // apply class change - applyClassChange = function() { - $.each( classAnimationActions, function(i, action) { - if ( value[ action ] ) { - animated[ action + "Class" ]( value[ action ] ); - } - }); - }; - applyClassChange(); - - // map all animated objects again - calculate new styles and diff - allAnimations = allAnimations.map(function() { - this.end = getElementStyles( this.el[ 0 ] ); - this.diff = styleDifference( this.start, this.end ); - return this; - }); - - // apply original class - animated.attr( "class", baseClass ); - - // map all animated objects again - this time collecting a promise - allAnimations = allAnimations.map(function() { - var styleInfo = this, - dfd = $.Deferred(), - opts = $.extend({}, o, { - queue: false, - complete: function() { - dfd.resolve( styleInfo ); - } - }); - - this.el.animate( this.diff, opts ); - return dfd.promise(); - }); - - // once all animations have completed: - $.when.apply( $, allAnimations.get() ).done(function() { - - // set the final class - applyClassChange(); - - // for each animated element, - // clear all css properties that were animated - $.each( arguments, function() { - var el = this.el; - $.each( this.diff, function(key) { - el.css( key, "" ); - }); - }); - - // this is guarnteed to be there if you use jQuery.speed() - // it also handles dequeuing the next anim... - o.complete.call( animated[ 0 ] ); - }); - }); -}; - -$.fn.extend({ - addClass: (function( orig ) { - return function( classNames, speed, easing, callback ) { - return speed ? - $.effects.animateClass.call( this, - { add: classNames }, speed, easing, callback ) : - orig.apply( this, arguments ); - }; - })( $.fn.addClass ), - - removeClass: (function( orig ) { - return function( classNames, speed, easing, callback ) { - return arguments.length > 1 ? - $.effects.animateClass.call( this, - { remove: classNames }, speed, easing, callback ) : - orig.apply( this, arguments ); - }; - })( $.fn.removeClass ), - - toggleClass: (function( orig ) { - return function( classNames, force, speed, easing, callback ) { - if ( typeof force === "boolean" || force === undefined ) { - if ( !speed ) { - // without speed parameter - return orig.apply( this, arguments ); - } else { - return $.effects.animateClass.call( this, - (force ? { add: classNames } : { remove: classNames }), - speed, easing, callback ); - } - } else { - // without force parameter - return $.effects.animateClass.call( this, - { toggle: classNames }, force, speed, easing ); - } - }; - })( $.fn.toggleClass ), - - switchClass: function( remove, add, speed, easing, callback) { - return $.effects.animateClass.call( this, { - add: add, - remove: remove - }, speed, easing, callback ); - } -}); - -})(); - -/******************************************************************************/ -/*********************************** EFFECTS **********************************/ -/******************************************************************************/ - -(function() { - -$.extend( $.effects, { - version: "1.10.3", - - // Saves a set of properties in a data storage - save: function( element, set ) { - for( var i=0; i < set.length; i++ ) { - if ( set[ i ] !== null ) { - element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] ); - } - } - }, - - // Restores a set of previously saved properties from a data storage - restore: function( element, set ) { - var val, i; - for( i=0; i < set.length; i++ ) { - if ( set[ i ] !== null ) { - val = element.data( dataSpace + set[ i ] ); - // support: jQuery 1.6.2 - // http://bugs.jquery.com/ticket/9917 - // jQuery 1.6.2 incorrectly returns undefined for any falsy value. - // We can't differentiate between "" and 0 here, so we just assume - // empty string since it's likely to be a more common value... - if ( val === undefined ) { - val = ""; - } - element.css( set[ i ], val ); - } - } - }, - - setMode: function( el, mode ) { - if (mode === "toggle") { - mode = el.is( ":hidden" ) ? "show" : "hide"; - } - return mode; - }, - - // Translates a [top,left] array into a baseline value - // this should be a little more flexible in the future to handle a string & hash - getBaseline: function( origin, original ) { - var y, x; - switch ( origin[ 0 ] ) { - case "top": y = 0; break; - case "middle": y = 0.5; break; - case "bottom": y = 1; break; - default: y = origin[ 0 ] / original.height; - } - switch ( origin[ 1 ] ) { - case "left": x = 0; break; - case "center": x = 0.5; break; - case "right": x = 1; break; - default: x = origin[ 1 ] / original.width; - } - return { - x: x, - y: y - }; - }, - - // Wraps the element around a wrapper that copies position properties - createWrapper: function( element ) { - - // if the element is already wrapped, return it - if ( element.parent().is( ".ui-effects-wrapper" )) { - return element.parent(); - } - - // wrap the element - var props = { - width: element.outerWidth(true), - height: element.outerHeight(true), - "float": element.css( "float" ) - }, - wrapper = $( "

" ) - .addClass( "ui-effects-wrapper" ) - .css({ - fontSize: "100%", - background: "transparent", - border: "none", - margin: 0, - padding: 0 - }), - // Store the size in case width/height are defined in % - Fixes #5245 - size = { - width: element.width(), - height: element.height() - }, - active = document.activeElement; - - // support: Firefox - // Firefox incorrectly exposes anonymous content - // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 - try { - active.id; - } catch( e ) { - active = document.body; - } - - element.wrap( wrapper ); - - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).focus(); - } - - wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually lose the reference to the wrapped element - - // transfer positioning properties to the wrapper - if ( element.css( "position" ) === "static" ) { - wrapper.css({ position: "relative" }); - element.css({ position: "relative" }); - } else { - $.extend( props, { - position: element.css( "position" ), - zIndex: element.css( "z-index" ) - }); - $.each([ "top", "left", "bottom", "right" ], function(i, pos) { - props[ pos ] = element.css( pos ); - if ( isNaN( parseInt( props[ pos ], 10 ) ) ) { - props[ pos ] = "auto"; - } - }); - element.css({ - position: "relative", - top: 0, - left: 0, - right: "auto", - bottom: "auto" - }); - } - element.css(size); - - return wrapper.css( props ).show(); - }, - - removeWrapper: function( element ) { - var active = document.activeElement; - - if ( element.parent().is( ".ui-effects-wrapper" ) ) { - element.parent().replaceWith( element ); - - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).focus(); - } - } - - - return element; - }, - - setTransition: function( element, list, factor, value ) { - value = value || {}; - $.each( list, function( i, x ) { - var unit = element.cssUnit( x ); - if ( unit[ 0 ] > 0 ) { - value[ x ] = unit[ 0 ] * factor + unit[ 1 ]; - } - }); - return value; - } -}); - -// return an effect options object for the given parameters: -function _normalizeArguments( effect, options, speed, callback ) { - - // allow passing all options as the first parameter - if ( $.isPlainObject( effect ) ) { - options = effect; - effect = effect.effect; - } - - // convert to an object - effect = { effect: effect }; - - // catch (effect, null, ...) - if ( options == null ) { - options = {}; - } - - // catch (effect, callback) - if ( $.isFunction( options ) ) { - callback = options; - speed = null; - options = {}; - } - - // catch (effect, speed, ?) - if ( typeof options === "number" || $.fx.speeds[ options ] ) { - callback = speed; - speed = options; - options = {}; - } - - // catch (effect, options, callback) - if ( $.isFunction( speed ) ) { - callback = speed; - speed = null; - } - - // add options to effect - if ( options ) { - $.extend( effect, options ); - } - - speed = speed || options.duration; - effect.duration = $.fx.off ? 0 : - typeof speed === "number" ? speed : - speed in $.fx.speeds ? $.fx.speeds[ speed ] : - $.fx.speeds._default; - - effect.complete = callback || options.complete; - - return effect; -} - -function standardAnimationOption( option ) { - // Valid standard speeds (nothing, number, named speed) - if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) { - return true; - } - - // Invalid strings - treat as "normal" speed - if ( typeof option === "string" && !$.effects.effect[ option ] ) { - return true; - } - - // Complete callback - if ( $.isFunction( option ) ) { - return true; - } - - // Options hash (but not naming an effect) - if ( typeof option === "object" && !option.effect ) { - return true; - } - - // Didn't match any standard API - return false; -} - -$.fn.extend({ - effect: function( /* effect, options, speed, callback */ ) { - var args = _normalizeArguments.apply( this, arguments ), - mode = args.mode, - queue = args.queue, - effectMethod = $.effects.effect[ args.effect ]; - - if ( $.fx.off || !effectMethod ) { - // delegate to the original method (e.g., .show()) if possible - if ( mode ) { - return this[ mode ]( args.duration, args.complete ); - } else { - return this.each( function() { - if ( args.complete ) { - args.complete.call( this ); - } - }); - } - } - - function run( next ) { - var elem = $( this ), - complete = args.complete, - mode = args.mode; - - function done() { - if ( $.isFunction( complete ) ) { - complete.call( elem[0] ); - } - if ( $.isFunction( next ) ) { - next(); - } - } - - // If the element already has the correct final state, delegate to - // the core methods so the internal tracking of "olddisplay" works. - if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { - elem[ mode ](); - done(); - } else { - effectMethod.call( elem[0], args, done ); - } - } - - return queue === false ? this.each( run ) : this.queue( queue || "fx", run ); - }, - - show: (function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "show"; - return this.effect.call( this, args ); - } - }; - })( $.fn.show ), - - hide: (function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "hide"; - return this.effect.call( this, args ); - } - }; - })( $.fn.hide ), - - toggle: (function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) || typeof option === "boolean" ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "toggle"; - return this.effect.call( this, args ); - } - }; - })( $.fn.toggle ), - - // helper functions - cssUnit: function(key) { - var style = this.css( key ), - val = []; - - $.each( [ "em", "px", "%", "pt" ], function( i, unit ) { - if ( style.indexOf( unit ) > 0 ) { - val = [ parseFloat( style ), unit ]; - } - }); - return val; - } -}); - -})(); - -/******************************************************************************/ -/*********************************** EASING ***********************************/ -/******************************************************************************/ - -(function() { - -// based on easing equations from Robert Penner (http://www.robertpenner.com/easing) - -var baseEasings = {}; - -$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) { - baseEasings[ name ] = function( p ) { - return Math.pow( p, i + 2 ); - }; -}); - -$.extend( baseEasings, { - Sine: function ( p ) { - return 1 - Math.cos( p * Math.PI / 2 ); - }, - Circ: function ( p ) { - return 1 - Math.sqrt( 1 - p * p ); - }, - Elastic: function( p ) { - return p === 0 || p === 1 ? p : - -Math.pow( 2, 8 * (p - 1) ) * Math.sin( ( (p - 1) * 80 - 7.5 ) * Math.PI / 15 ); - }, - Back: function( p ) { - return p * p * ( 3 * p - 2 ); - }, - Bounce: function ( p ) { - var pow2, - bounce = 4; - - while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} - return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); - } -}); - -$.each( baseEasings, function( name, easeIn ) { - $.easing[ "easeIn" + name ] = easeIn; - $.easing[ "easeOut" + name ] = function( p ) { - return 1 - easeIn( 1 - p ); - }; - $.easing[ "easeInOut" + name ] = function( p ) { - return p < 0.5 ? - easeIn( p * 2 ) / 2 : - 1 - easeIn( p * -2 + 2 ) / 2; - }; -}); - -})(); - -})(jQuery); - -(function( $, undefined ) { - -var uid = 0, - hideProps = {}, - showProps = {}; - -hideProps.height = hideProps.paddingTop = hideProps.paddingBottom = - hideProps.borderTopWidth = hideProps.borderBottomWidth = "hide"; -showProps.height = showProps.paddingTop = showProps.paddingBottom = - showProps.borderTopWidth = showProps.borderBottomWidth = "show"; - -$.widget( "ui.accordion", { - version: "1.10.3", - options: { - active: 0, - animate: {}, - collapsible: false, - event: "click", - header: "> li > :first-child,> :not(li):even", - heightStyle: "auto", - icons: { - activeHeader: "ui-icon-triangle-1-s", - header: "ui-icon-triangle-1-e" - }, - - // callbacks - activate: null, - beforeActivate: null - }, - - _create: function() { - var options = this.options; - this.prevShow = this.prevHide = $(); - this.element.addClass( "ui-accordion ui-widget ui-helper-reset" ) - // ARIA - .attr( "role", "tablist" ); - - // don't allow collapsible: false and active: false / null - if ( !options.collapsible && (options.active === false || options.active == null) ) { - options.active = 0; - } - - this._processPanels(); - // handle negative values - if ( options.active < 0 ) { - options.active += this.headers.length; - } - this._refresh(); - }, - - _getCreateEventData: function() { - return { - header: this.active, - panel: !this.active.length ? $() : this.active.next(), - content: !this.active.length ? $() : this.active.next() - }; - }, - - _createIcons: function() { - var icons = this.options.icons; - if ( icons ) { - $( "" ) - .addClass( "ui-accordion-header-icon ui-icon " + icons.header ) - .prependTo( this.headers ); - this.active.children( ".ui-accordion-header-icon" ) - .removeClass( icons.header ) - .addClass( icons.activeHeader ); - this.headers.addClass( "ui-accordion-icons" ); - } - }, - - _destroyIcons: function() { - this.headers - .removeClass( "ui-accordion-icons" ) - .children( ".ui-accordion-header-icon" ) - .remove(); - }, - - _destroy: function() { - var contents; - - // clean up main element - this.element - .removeClass( "ui-accordion ui-widget ui-helper-reset" ) - .removeAttr( "role" ); - - // clean up headers - this.headers - .removeClass( "ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" ) - .removeAttr( "role" ) - .removeAttr( "aria-selected" ) - .removeAttr( "aria-controls" ) - .removeAttr( "tabIndex" ) - .each(function() { - if ( /^ui-accordion/.test( this.id ) ) { - this.removeAttribute( "id" ); - } - }); - this._destroyIcons(); - - // clean up content panels - contents = this.headers.next() - .css( "display", "" ) - .removeAttr( "role" ) - .removeAttr( "aria-expanded" ) - .removeAttr( "aria-hidden" ) - .removeAttr( "aria-labelledby" ) - .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled" ) - .each(function() { - if ( /^ui-accordion/.test( this.id ) ) { - this.removeAttribute( "id" ); - } - }); - if ( this.options.heightStyle !== "content" ) { - contents.css( "height", "" ); - } - }, - - _setOption: function( key, value ) { - if ( key === "active" ) { - // _activate() will handle invalid values and update this.options - this._activate( value ); - return; - } - - if ( key === "event" ) { - if ( this.options.event ) { - this._off( this.headers, this.options.event ); - } - this._setupEvents( value ); - } - - this._super( key, value ); - - // setting collapsible: false while collapsed; open first panel - if ( key === "collapsible" && !value && this.options.active === false ) { - this._activate( 0 ); - } - - if ( key === "icons" ) { - this._destroyIcons(); - if ( value ) { - this._createIcons(); - } - } - - // #5332 - opacity doesn't cascade to positioned elements in IE - // so we need to add the disabled class to the headers and panels - if ( key === "disabled" ) { - this.headers.add( this.headers.next() ) - .toggleClass( "ui-state-disabled", !!value ); - } - }, - - _keydown: function( event ) { - /*jshint maxcomplexity:15*/ - if ( event.altKey || event.ctrlKey ) { - return; - } - - var keyCode = $.ui.keyCode, - length = this.headers.length, - currentIndex = this.headers.index( event.target ), - toFocus = false; - - switch ( event.keyCode ) { - case keyCode.RIGHT: - case keyCode.DOWN: - toFocus = this.headers[ ( currentIndex + 1 ) % length ]; - break; - case keyCode.LEFT: - case keyCode.UP: - toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; - break; - case keyCode.SPACE: - case keyCode.ENTER: - this._eventHandler( event ); - break; - case keyCode.HOME: - toFocus = this.headers[ 0 ]; - break; - case keyCode.END: - toFocus = this.headers[ length - 1 ]; - break; - } - - if ( toFocus ) { - $( event.target ).attr( "tabIndex", -1 ); - $( toFocus ).attr( "tabIndex", 0 ); - toFocus.focus(); - event.preventDefault(); - } - }, - - _panelKeyDown : function( event ) { - if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { - $( event.currentTarget ).prev().focus(); - } - }, - - refresh: function() { - var options = this.options; - this._processPanels(); - - // was collapsed or no panel - if ( ( options.active === false && options.collapsible === true ) || !this.headers.length ) { - options.active = false; - this.active = $(); - // active false only when collapsible is true - } else if ( options.active === false ) { - this._activate( 0 ); - // was active, but active panel is gone - } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { - // all remaining panel are disabled - if ( this.headers.length === this.headers.find(".ui-state-disabled").length ) { - options.active = false; - this.active = $(); - // activate previous panel - } else { - this._activate( Math.max( 0, options.active - 1 ) ); - } - // was active, active panel still exists - } else { - // make sure active index is correct - options.active = this.headers.index( this.active ); - } - - this._destroyIcons(); - - this._refresh(); - }, - - _processPanels: function() { - this.headers = this.element.find( this.options.header ) - .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" ); - - this.headers.next() - .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" ) - .filter(":not(.ui-accordion-content-active)") - .hide(); - }, - - _refresh: function() { - var maxHeight, - options = this.options, - heightStyle = options.heightStyle, - parent = this.element.parent(), - accordionId = this.accordionId = "ui-accordion-" + - (this.element.attr( "id" ) || ++uid); - - this.active = this._findActive( options.active ) - .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ) - .removeClass( "ui-corner-all" ); - this.active.next() - .addClass( "ui-accordion-content-active" ) - .show(); - - this.headers - .attr( "role", "tab" ) - .each(function( i ) { - var header = $( this ), - headerId = header.attr( "id" ), - panel = header.next(), - panelId = panel.attr( "id" ); - if ( !headerId ) { - headerId = accordionId + "-header-" + i; - header.attr( "id", headerId ); - } - if ( !panelId ) { - panelId = accordionId + "-panel-" + i; - panel.attr( "id", panelId ); - } - header.attr( "aria-controls", panelId ); - panel.attr( "aria-labelledby", headerId ); - }) - .next() - .attr( "role", "tabpanel" ); - - this.headers - .not( this.active ) - .attr({ - "aria-selected": "false", - tabIndex: -1 - }) - .next() - .attr({ - "aria-expanded": "false", - "aria-hidden": "true" - }) - .hide(); - - // make sure at least one header is in the tab order - if ( !this.active.length ) { - this.headers.eq( 0 ).attr( "tabIndex", 0 ); - } else { - this.active.attr({ - "aria-selected": "true", - tabIndex: 0 - }) - .next() - .attr({ - "aria-expanded": "true", - "aria-hidden": "false" - }); - } - - this._createIcons(); - - this._setupEvents( options.event ); - - if ( heightStyle === "fill" ) { - maxHeight = parent.height(); - this.element.siblings( ":visible" ).each(function() { - var elem = $( this ), - position = elem.css( "position" ); - - if ( position === "absolute" || position === "fixed" ) { - return; - } - maxHeight -= elem.outerHeight( true ); - }); - - this.headers.each(function() { - maxHeight -= $( this ).outerHeight( true ); - }); - - this.headers.next() - .each(function() { - $( this ).height( Math.max( 0, maxHeight - - $( this ).innerHeight() + $( this ).height() ) ); - }) - .css( "overflow", "auto" ); - } else if ( heightStyle === "auto" ) { - maxHeight = 0; - this.headers.next() - .each(function() { - maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); - }) - .height( maxHeight ); - } - }, - - _activate: function( index ) { - var active = this._findActive( index )[ 0 ]; - - // trying to activate the already active panel - if ( active === this.active[ 0 ] ) { - return; - } - - // trying to collapse, simulate a click on the currently active header - active = active || this.active[ 0 ]; - - this._eventHandler({ - target: active, - currentTarget: active, - preventDefault: $.noop - }); - }, - - _findActive: function( selector ) { - return typeof selector === "number" ? this.headers.eq( selector ) : $(); - }, - - _setupEvents: function( event ) { - var events = { - keydown: "_keydown" - }; - if ( event ) { - $.each( event.split(" "), function( index, eventName ) { - events[ eventName ] = "_eventHandler"; - }); - } - - this._off( this.headers.add( this.headers.next() ) ); - this._on( this.headers, events ); - this._on( this.headers.next(), { keydown: "_panelKeyDown" }); - this._hoverable( this.headers ); - this._focusable( this.headers ); - }, - - _eventHandler: function( event ) { - var options = this.options, - active = this.active, - clicked = $( event.currentTarget ), - clickedIsActive = clicked[ 0 ] === active[ 0 ], - collapsing = clickedIsActive && options.collapsible, - toShow = collapsing ? $() : clicked.next(), - toHide = active.next(), - eventData = { - oldHeader: active, - oldPanel: toHide, - newHeader: collapsing ? $() : clicked, - newPanel: toShow - }; - - event.preventDefault(); - - if ( - // click on active header, but not collapsible - ( clickedIsActive && !options.collapsible ) || - // allow canceling activation - ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { - return; - } - - options.active = collapsing ? false : this.headers.index( clicked ); - - // when the call to ._toggle() comes after the class changes - // it causes a very odd bug in IE 8 (see #6720) - this.active = clickedIsActive ? $() : clicked; - this._toggle( eventData ); - - // switch classes - // corner classes on the previously active header stay after the animation - active.removeClass( "ui-accordion-header-active ui-state-active" ); - if ( options.icons ) { - active.children( ".ui-accordion-header-icon" ) - .removeClass( options.icons.activeHeader ) - .addClass( options.icons.header ); - } - - if ( !clickedIsActive ) { - clicked - .removeClass( "ui-corner-all" ) - .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ); - if ( options.icons ) { - clicked.children( ".ui-accordion-header-icon" ) - .removeClass( options.icons.header ) - .addClass( options.icons.activeHeader ); - } - - clicked - .next() - .addClass( "ui-accordion-content-active" ); - } - }, - - _toggle: function( data ) { - var toShow = data.newPanel, - toHide = this.prevShow.length ? this.prevShow : data.oldPanel; - - // handle activating a panel during the animation for another activation - this.prevShow.add( this.prevHide ).stop( true, true ); - this.prevShow = toShow; - this.prevHide = toHide; - - if ( this.options.animate ) { - this._animate( toShow, toHide, data ); - } else { - toHide.hide(); - toShow.show(); - this._toggleComplete( data ); - } - - toHide.attr({ - "aria-expanded": "false", - "aria-hidden": "true" - }); - toHide.prev().attr( "aria-selected", "false" ); - // if we're switching panels, remove the old header from the tab order - // if we're opening from collapsed state, remove the previous header from the tab order - // if we're collapsing, then keep the collapsing header in the tab order - if ( toShow.length && toHide.length ) { - toHide.prev().attr( "tabIndex", -1 ); - } else if ( toShow.length ) { - this.headers.filter(function() { - return $( this ).attr( "tabIndex" ) === 0; - }) - .attr( "tabIndex", -1 ); - } - - toShow - .attr({ - "aria-expanded": "true", - "aria-hidden": "false" - }) - .prev() - .attr({ - "aria-selected": "true", - tabIndex: 0 - }); - }, - - _animate: function( toShow, toHide, data ) { - var total, easing, duration, - that = this, - adjust = 0, - down = toShow.length && - ( !toHide.length || ( toShow.index() < toHide.index() ) ), - animate = this.options.animate || {}, - options = down && animate.down || animate, - complete = function() { - that._toggleComplete( data ); - }; - - if ( typeof options === "number" ) { - duration = options; - } - if ( typeof options === "string" ) { - easing = options; - } - // fall back from options to animation in case of partial down settings - easing = easing || options.easing || animate.easing; - duration = duration || options.duration || animate.duration; - - if ( !toHide.length ) { - return toShow.animate( showProps, duration, easing, complete ); - } - if ( !toShow.length ) { - return toHide.animate( hideProps, duration, easing, complete ); - } - - total = toShow.show().outerHeight(); - toHide.animate( hideProps, { - duration: duration, - easing: easing, - step: function( now, fx ) { - fx.now = Math.round( now ); - } - }); - toShow - .hide() - .animate( showProps, { - duration: duration, - easing: easing, - complete: complete, - step: function( now, fx ) { - fx.now = Math.round( now ); - if ( fx.prop !== "height" ) { - adjust += fx.now; - } else if ( that.options.heightStyle !== "content" ) { - fx.now = Math.round( total - toHide.outerHeight() - adjust ); - adjust = 0; - } - } - }); - }, - - _toggleComplete: function( data ) { - var toHide = data.oldPanel; - - toHide - .removeClass( "ui-accordion-content-active" ) - .prev() - .removeClass( "ui-corner-top" ) - .addClass( "ui-corner-all" ); - - // Work around for rendering bug in IE (#5421) - if ( toHide.length ) { - toHide.parent()[0].className = toHide.parent()[0].className; - } - - this._trigger( "activate", null, data ); - } -}); - -})( jQuery ); - -(function( $, undefined ) { - -// used to prevent race conditions with remote data sources -var requestIndex = 0; - -$.widget( "ui.autocomplete", { - version: "1.10.3", - defaultElement: "", - options: { - appendTo: null, - autoFocus: false, - delay: 300, - minLength: 1, - position: { - my: "left top", - at: "left bottom", - collision: "none" - }, - source: null, - - // callbacks - change: null, - close: null, - focus: null, - open: null, - response: null, - search: null, - select: null - }, - - pending: 0, - - _create: function() { - // Some browsers only repeat keydown events, not keypress events, - // so we use the suppressKeyPress flag to determine if we've already - // handled the keydown event. #7269 - // Unfortunately the code for & in keypress is the same as the up arrow, - // so we use the suppressKeyPressRepeat flag to avoid handling keypress - // events when we know the keydown event was used to modify the - // search term. #7799 - var suppressKeyPress, suppressKeyPressRepeat, suppressInput, - nodeName = this.element[0].nodeName.toLowerCase(), - isTextarea = nodeName === "textarea", - isInput = nodeName === "input"; - - this.isMultiLine = - // Textareas are always multi-line - isTextarea ? true : - // Inputs are always single-line, even if inside a contentEditable element - // IE also treats inputs as contentEditable - isInput ? false : - // All other element types are determined by whether or not they're contentEditable - this.element.prop( "isContentEditable" ); - - this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; - this.isNewMenu = true; - - this.element - .addClass( "ui-autocomplete-input" ) - .attr( "autocomplete", "off" ); - - this._on( this.element, { - keydown: function( event ) { - /*jshint maxcomplexity:15*/ - if ( this.element.prop( "readOnly" ) ) { - suppressKeyPress = true; - suppressInput = true; - suppressKeyPressRepeat = true; - return; - } - - suppressKeyPress = false; - suppressInput = false; - suppressKeyPressRepeat = false; - var keyCode = $.ui.keyCode; - switch( event.keyCode ) { - case keyCode.PAGE_UP: - suppressKeyPress = true; - this._move( "previousPage", event ); - break; - case keyCode.PAGE_DOWN: - suppressKeyPress = true; - this._move( "nextPage", event ); - break; - case keyCode.UP: - suppressKeyPress = true; - this._keyEvent( "previous", event ); - break; - case keyCode.DOWN: - suppressKeyPress = true; - this._keyEvent( "next", event ); - break; - case keyCode.ENTER: - case keyCode.NUMPAD_ENTER: - // when menu is open and has focus - if ( this.menu.active ) { - // #6055 - Opera still allows the keypress to occur - // which causes forms to submit - suppressKeyPress = true; - event.preventDefault(); - this.menu.select( event ); - } - break; - case keyCode.TAB: - if ( this.menu.active ) { - this.menu.select( event ); - } - break; - case keyCode.ESCAPE: - if ( this.menu.element.is( ":visible" ) ) { - this._value( this.term ); - this.close( event ); - // Different browsers have different default behavior for escape - // Single press can mean undo or clear - // Double press in IE means clear the whole form - event.preventDefault(); - } - break; - default: - suppressKeyPressRepeat = true; - // search timeout should be triggered before the input value is changed - this._searchTimeout( event ); - break; - } - }, - keypress: function( event ) { - if ( suppressKeyPress ) { - suppressKeyPress = false; - if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { - event.preventDefault(); - } - return; - } - if ( suppressKeyPressRepeat ) { - return; - } - - // replicate some key handlers to allow them to repeat in Firefox and Opera - var keyCode = $.ui.keyCode; - switch( event.keyCode ) { - case keyCode.PAGE_UP: - this._move( "previousPage", event ); - break; - case keyCode.PAGE_DOWN: - this._move( "nextPage", event ); - break; - case keyCode.UP: - this._keyEvent( "previous", event ); - break; - case keyCode.DOWN: - this._keyEvent( "next", event ); - break; - } - }, - input: function( event ) { - if ( suppressInput ) { - suppressInput = false; - event.preventDefault(); - return; - } - this._searchTimeout( event ); - }, - focus: function() { - this.selectedItem = null; - this.previous = this._value(); - }, - blur: function( event ) { - if ( this.cancelBlur ) { - delete this.cancelBlur; - return; - } - - clearTimeout( this.searching ); - this.close( event ); - this._change( event ); - } - }); - - this._initSource(); - this.menu = $( "