Compare commits
No commits in common. "main" and "use-head-request-for-hash" have entirely different histories.
main
...
use-head-r
|
|
@ -1,15 +0,0 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/ruby:1-3.1-bullseye
|
|
||||||
|
|
||||||
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
|
|
||||||
# The value is a comma-separated list of allowed domains
|
|
||||||
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment this line to install additional gems.
|
|
||||||
# RUN gem install <your-gem-names-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment this line to install global node packages.
|
|
||||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE DATABASE openneo_impress;
|
|
||||||
GRANT ALL PRIVILEGES ON openneo_impress.* TO impress_dev;
|
|
||||||
|
|
||||||
CREATE DATABASE openneo_id;
|
|
||||||
GRANT ALL PRIVILEGES ON openneo_id.* TO impress_dev;
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ruby-rails-postgres
|
|
||||||
{
|
|
||||||
"name": "Dress to Impress",
|
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
|
||||||
"service": "app",
|
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
|
||||||
"nodeGypDependencies": true,
|
|
||||||
"version": "lts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
// "features": {},
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// This can be used to network with other containers or the host.
|
|
||||||
"forwardPorts": [3000],
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": ".devcontainer/post-create.sh",
|
|
||||||
|
|
||||||
"containerEnv": {
|
|
||||||
// Because the database is hosted on the local network at the hostname `db`,
|
|
||||||
// we partially override `config/database.yml` to connect to `db`!
|
|
||||||
"DATABASE_URL_PRIMARY_DEV": "mysql2://db",
|
|
||||||
"DATABASE_URL_OPENNEO_ID_DEV": "mysql2://db",
|
|
||||||
"DATABASE_URL_PRIMARY_TEST": "mysql2://db",
|
|
||||||
"DATABASE_URL_OPENNEO_ID_TEST": "mysql2://db",
|
|
||||||
|
|
||||||
// HACK: Out of the box, this dev container doesn't allow installation to
|
|
||||||
// the default GEM_HOME, because of a weird thing going on with RVM.
|
|
||||||
// Instead, we set a custom GEM_HOME and GEM_PATH in our home directory!
|
|
||||||
// https://github.com/devcontainers/templates/issues/188
|
|
||||||
"GEM_HOME": "~/.rubygems",
|
|
||||||
"GEM_PATH": "~/.rubygems"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: .devcontainer/Dockerfile
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ../..:/workspaces:cached
|
|
||||||
|
|
||||||
# Overrides default command so things don't shut down after the process ends.
|
|
||||||
command: sleep infinity
|
|
||||||
|
|
||||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
|
||||||
network_mode: service:db
|
|
||||||
|
|
||||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
|
||||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: mysql:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./create-db.sql:/docker-entrypoint-initdb.d/create-db.sql
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: impress_dev
|
|
||||||
MYSQL_USER: impress_dev
|
|
||||||
MYSQL_PASSWORD: impress_dev
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e # Quit if any part of this script fails.
|
|
||||||
|
|
||||||
# Mark all git repositories as safe to execute, including cached gems.
|
|
||||||
# NOTE: This would be dangerous to run on a normal multi-user machine,
|
|
||||||
# but for a dev container that only we use, it should be fine!
|
|
||||||
git config --global safe.directory '*'
|
|
||||||
|
|
||||||
# Install the app's Ruby gem dependencies.
|
|
||||||
bundle install
|
|
||||||
|
|
||||||
# Set up the databases: create the schema, and load in some default data.
|
|
||||||
bin/rails db:schema:load db:seed
|
|
||||||
|
|
||||||
# Install the app's JS dependencies.
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Run a first-time build of the app's JS, in development mode.
|
|
||||||
yarn build:dev
|
|
||||||
|
|
@ -1,20 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals"],
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:jsx-a11y/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"process": true // For process.env["NODE_ENV"]
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": [
|
"no-console": [
|
||||||
"warn",
|
"warn",
|
||||||
|
|
@ -34,12 +21,6 @@
|
||||||
],
|
],
|
||||||
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
|
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
|
||||||
// We have some React.forwardRefs that trigger this, not sure how to improve
|
// We have some React.forwardRefs that trigger this, not sure how to improve
|
||||||
"react/display-name": "off",
|
"react/display-name": "off"
|
||||||
"react/prop-types": "off"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.sql.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
21
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sentry:
|
||||||
|
name: Checkout latest, and create Sentry release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Create Sentry release
|
||||||
|
uses: getsentry/action-release@v1.1.5
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
with:
|
||||||
|
environment: production
|
||||||
49
.gitignore
vendored
|
|
@ -1,23 +1,34 @@
|
||||||
.bundle
|
# dependencies
|
||||||
db/*.sqlite3
|
|
||||||
log/*.log
|
|
||||||
tmp/**/*
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
/app/assets/builds/*
|
|
||||||
!/app/assets/builds/.keep
|
|
||||||
|
|
||||||
/public/public-data
|
|
||||||
|
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn
|
||||||
|
|
||||||
.pnp.*
|
# testing
|
||||||
.yarn/*
|
/coverage
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.vercel
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
*.pem
|
||||||
|
# debug
|
||||||
|
# local env files
|
||||||
|
# vercel
|
||||||
1
.husky/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
_
|
||||||
3
.husky/post-checkout
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
|
||||||
|
git lfs post-checkout "$@"
|
||||||
3
.husky/post-commit
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-commit.\n"; exit 2; }
|
||||||
|
git lfs post-commit "$@"
|
||||||
3
.husky/post-merge
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-merge.\n"; exit 2; }
|
||||||
|
git lfs post-merge "$@"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint --max-warnings=0 --fix
|
yarn lint-staged
|
||||||
|
|
|
||||||
3
.husky/pre-push
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
|
||||||
|
git lfs pre-push "$@"
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/app/assets/javascripts/lib
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3.3.4
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
include:
|
|
||||||
- "app/**/*.rb"
|
|
||||||
- "config/**/*.rb"
|
|
||||||
exclude:
|
|
||||||
- "spec/**/*"
|
|
||||||
- "test/**/*"
|
|
||||||
- "vendor/**/*"
|
|
||||||
- ".bundle/**/*"
|
|
||||||
require:
|
|
||||||
- actioncable
|
|
||||||
- actionmailer
|
|
||||||
- actionpack
|
|
||||||
- actionview
|
|
||||||
- activemodel
|
|
||||||
- activerecord
|
|
||||||
- activesupport
|
|
||||||
plugins:
|
|
||||||
- solargraph-rails
|
|
||||||
domains: []
|
|
||||||
reporters:
|
|
||||||
- require_not_found
|
|
||||||
require_paths: []
|
|
||||||
max_files: 5000
|
|
||||||
41
.vscode/impress-2020.code-snippets
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
// Place your impress-2020 workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||||
|
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||||
|
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||||
|
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||||
|
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||||
|
// Placeholders with the same ids are connected.
|
||||||
|
// Example:
|
||||||
|
// "Print to console": {
|
||||||
|
// "scope": "javascript,typescript",
|
||||||
|
// "prefix": "log",
|
||||||
|
// "body": [
|
||||||
|
// "console.log('$1');",
|
||||||
|
// "$2"
|
||||||
|
// ],
|
||||||
|
// "description": "Log output to console"
|
||||||
|
// }
|
||||||
|
"Component file": {
|
||||||
|
"scope": "javascript",
|
||||||
|
"prefix": "componentfile",
|
||||||
|
"body": [
|
||||||
|
"import React from \"react\";",
|
||||||
|
"import { Box } from \"@chakra-ui/react\";",
|
||||||
|
"",
|
||||||
|
"function $TM_FILENAME_BASE() {",
|
||||||
|
" return <Box>$1</Box>;",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"export default $TM_FILENAME_BASE;"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Function component": {
|
||||||
|
"scope": "javascript",
|
||||||
|
"prefix": "fncomponent",
|
||||||
|
"body": [
|
||||||
|
"function ${1:Component}({${2:children}}) {",
|
||||||
|
" return ${3:<Box>$4</Box>};",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"jest.pathToJest": "yarn test",
|
||||||
|
"jest.autoEnable": false,
|
||||||
|
"javascript.suggest.completeJSDocs": false,
|
||||||
|
"typescript.suggest.completeJSDocs": false,
|
||||||
|
"editor.rulers": [80]
|
||||||
|
}
|
||||||
91
Gemfile
|
|
@ -1,91 +0,0 @@
|
||||||
source 'https://rubygems.org'
|
|
||||||
ruby '3.3.4'
|
|
||||||
|
|
||||||
gem 'rails', '~> 7.1', '>= 7.1.3.4'
|
|
||||||
|
|
||||||
# The HTTP server running the Rails instance.
|
|
||||||
gem 'falcon', '~> 0.48.0'
|
|
||||||
|
|
||||||
# Our database is MySQL, in both development and production.
|
|
||||||
gem 'mysql2', '~> 0.5.5'
|
|
||||||
|
|
||||||
# For reading the .env file, which you can use in development to more easily
|
|
||||||
# set environment variables for secret data.
|
|
||||||
gem 'dotenv-rails', '~> 2.8', '>= 2.8.1'
|
|
||||||
|
|
||||||
# For the asset pipeline: templates, CSS, JS, etc.
|
|
||||||
gem 'sprockets', '~> 4.2'
|
|
||||||
gem 'haml', '~> 6.1', '>= 6.1.1'
|
|
||||||
gem 'sass-rails', '~> 6.0'
|
|
||||||
gem 'terser', '~> 1.1', '>= 1.1.17'
|
|
||||||
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
|
||||||
gem 'jsbundling-rails', '~> 1.1'
|
|
||||||
gem 'turbo-rails', '~> 2.0'
|
|
||||||
|
|
||||||
# For authentication.
|
|
||||||
gem 'devise', '~> 4.9', '>= 4.9.2'
|
|
||||||
gem 'devise-encryptable', '~> 0.2.0'
|
|
||||||
gem 'omniauth', '~> 2.1'
|
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
|
||||||
gem "omniauth_openid_connect", "~> 0.7.1"
|
|
||||||
|
|
||||||
# For pagination UI.
|
|
||||||
gem 'will_paginate', '~> 4.0'
|
|
||||||
|
|
||||||
# For translation, both for the site UI and for Neopets data.
|
|
||||||
gem 'rails-i18n', '~> 7.0', '>= 7.0.7'
|
|
||||||
gem 'http_accept_language', '~> 2.1', '>= 2.1.1'
|
|
||||||
|
|
||||||
# For reading and parsing HTML from Neopets.com, like importing Closet pages.
|
|
||||||
gem 'nokogiri', '~> 1.15', '>= 1.15.3'
|
|
||||||
|
|
||||||
# For safely rendering users' Markdown + HTML on item list pages.
|
|
||||||
gem 'rdiscount', '~> 2.2', '>= 2.2.7.1'
|
|
||||||
gem 'sanitize', '~> 6.0', '>= 6.0.2'
|
|
||||||
|
|
||||||
# For working with Neopets APIs.
|
|
||||||
# unstable version of RocketAMF interprets info registry as a hash instead of an array
|
|
||||||
gem 'RocketAMF', :git => 'https://github.com/rubyamf/rocketamf.git'
|
|
||||||
|
|
||||||
# For preventing too many modeling attempts.
|
|
||||||
gem 'rack-attack', '~> 6.7'
|
|
||||||
|
|
||||||
# For testing emails in development.
|
|
||||||
gem 'letter_opener', '~> 1.8', '>= 1.8.1', group: :development
|
|
||||||
|
|
||||||
# For parallel API calls.
|
|
||||||
gem 'parallel', '~> 1.23'
|
|
||||||
|
|
||||||
# For miscellaneous HTTP requests.
|
|
||||||
gem "httparty", "~> 0.22.0"
|
|
||||||
gem "addressable", "~> 2.8"
|
|
||||||
|
|
||||||
# For advanced batching of many HTTP requests.
|
|
||||||
gem "async", "~> 2.17", require: false
|
|
||||||
gem "async-http", "~> 0.75.0", require: false
|
|
||||||
gem "thread-local", "~> 1.1", require: false
|
|
||||||
|
|
||||||
# For debugging.
|
|
||||||
gem 'web-console', '~> 4.2', group: :development
|
|
||||||
|
|
||||||
# TODO: Review our use of content_tag_for etc and uninstall this!
|
|
||||||
gem 'record_tag_helper', '~> 1.0', '>= 1.0.1'
|
|
||||||
|
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
|
||||||
gem 'bootsnap', '~> 1.16', require: false
|
|
||||||
|
|
||||||
# For investigating performance issues.
|
|
||||||
gem "rack-mini-profiler", "~> 3.1"
|
|
||||||
gem "memory_profiler", "~> 1.0"
|
|
||||||
gem "stackprof", "~> 0.2.25"
|
|
||||||
|
|
||||||
# For monitoring errors in production.
|
|
||||||
gem "sentry-ruby", "~> 5.12"
|
|
||||||
gem "sentry-rails", "~> 5.12"
|
|
||||||
|
|
||||||
# For tasks that use shell commands.
|
|
||||||
gem "shell", "~> 0.8.1"
|
|
||||||
|
|
||||||
# For workspace autocomplete.
|
|
||||||
gem "solargraph", "~> 0.50.0", group: :development
|
|
||||||
gem "solargraph-rails", "~> 1.1", group: :development
|
|
||||||
546
Gemfile.lock
|
|
@ -1,546 +0,0 @@
|
||||||
GIT
|
|
||||||
remote: https://github.com/rubyamf/rocketamf.git
|
|
||||||
revision: 796f591d002b5cf47df436dbcbd6f2ab00e869ed
|
|
||||||
specs:
|
|
||||||
RocketAMF (1.0.0)
|
|
||||||
|
|
||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
actioncable (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
nio4r (~> 2.0)
|
|
||||||
websocket-driver (>= 0.6.1)
|
|
||||||
zeitwerk (~> 2.6)
|
|
||||||
actionmailbox (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
activejob (= 7.2.1)
|
|
||||||
activerecord (= 7.2.1)
|
|
||||||
activestorage (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
mail (>= 2.8.0)
|
|
||||||
actionmailer (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
actionview (= 7.2.1)
|
|
||||||
activejob (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
mail (>= 2.8.0)
|
|
||||||
rails-dom-testing (~> 2.2)
|
|
||||||
actionpack (7.2.1)
|
|
||||||
actionview (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
nokogiri (>= 1.8.5)
|
|
||||||
racc
|
|
||||||
rack (>= 2.2.4, < 3.2)
|
|
||||||
rack-session (>= 1.0.1)
|
|
||||||
rack-test (>= 0.6.3)
|
|
||||||
rails-dom-testing (~> 2.2)
|
|
||||||
rails-html-sanitizer (~> 1.6)
|
|
||||||
useragent (~> 0.16)
|
|
||||||
actiontext (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
activerecord (= 7.2.1)
|
|
||||||
activestorage (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
globalid (>= 0.6.0)
|
|
||||||
nokogiri (>= 1.8.5)
|
|
||||||
actionview (7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
builder (~> 3.1)
|
|
||||||
erubi (~> 1.11)
|
|
||||||
rails-dom-testing (~> 2.2)
|
|
||||||
rails-html-sanitizer (~> 1.6)
|
|
||||||
activejob (7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
globalid (>= 0.3.6)
|
|
||||||
activemodel (7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
activerecord (7.2.1)
|
|
||||||
activemodel (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
timeout (>= 0.4.0)
|
|
||||||
activestorage (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
activejob (= 7.2.1)
|
|
||||||
activerecord (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
marcel (~> 1.0)
|
|
||||||
activesupport (7.2.1)
|
|
||||||
base64
|
|
||||||
bigdecimal
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
||||||
connection_pool (>= 2.2.5)
|
|
||||||
drb
|
|
||||||
i18n (>= 1.6, < 2)
|
|
||||||
logger (>= 1.4.2)
|
|
||||||
minitest (>= 5.1)
|
|
||||||
securerandom (>= 0.3)
|
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
|
||||||
addressable (2.8.7)
|
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
|
||||||
aes_key_wrap (1.1.0)
|
|
||||||
ast (2.4.2)
|
|
||||||
async (2.17.0)
|
|
||||||
console (~> 1.26)
|
|
||||||
fiber-annotation
|
|
||||||
io-event (~> 1.6, >= 1.6.5)
|
|
||||||
async-container (0.18.3)
|
|
||||||
async (~> 2.10)
|
|
||||||
async-http (0.75.0)
|
|
||||||
async (>= 2.10.2)
|
|
||||||
async-pool (~> 0.7)
|
|
||||||
io-endpoint (~> 0.11)
|
|
||||||
io-stream (~> 0.4)
|
|
||||||
protocol-http (~> 0.30)
|
|
||||||
protocol-http1 (~> 0.20)
|
|
||||||
protocol-http2 (~> 0.18)
|
|
||||||
traces (>= 0.10)
|
|
||||||
async-http-cache (0.4.4)
|
|
||||||
async-http (~> 0.56)
|
|
||||||
async-pool (0.8.1)
|
|
||||||
async (>= 1.25)
|
|
||||||
metrics
|
|
||||||
traces
|
|
||||||
async-service (0.12.0)
|
|
||||||
async
|
|
||||||
async-container (~> 0.16)
|
|
||||||
attr_required (1.0.2)
|
|
||||||
babel-source (5.8.35)
|
|
||||||
babel-transpiler (0.7.0)
|
|
||||||
babel-source (>= 4.0, < 6)
|
|
||||||
execjs (~> 2.0)
|
|
||||||
backport (1.2.0)
|
|
||||||
base64 (0.2.0)
|
|
||||||
bcrypt (3.1.20)
|
|
||||||
benchmark (0.3.0)
|
|
||||||
bigdecimal (3.1.8)
|
|
||||||
bindata (2.5.0)
|
|
||||||
bindex (0.8.1)
|
|
||||||
bootsnap (1.18.4)
|
|
||||||
msgpack (~> 1.2)
|
|
||||||
builder (3.3.0)
|
|
||||||
childprocess (5.1.0)
|
|
||||||
logger (~> 1.5)
|
|
||||||
concurrent-ruby (1.3.4)
|
|
||||||
connection_pool (2.4.1)
|
|
||||||
console (1.27.0)
|
|
||||||
fiber-annotation
|
|
||||||
fiber-local (~> 1.1)
|
|
||||||
json
|
|
||||||
crass (1.0.6)
|
|
||||||
csv (3.3.0)
|
|
||||||
date (3.3.4)
|
|
||||||
devise (4.9.4)
|
|
||||||
bcrypt (~> 3.0)
|
|
||||||
orm_adapter (~> 0.1)
|
|
||||||
railties (>= 4.1.0)
|
|
||||||
responders
|
|
||||||
warden (~> 1.2.3)
|
|
||||||
devise-encryptable (0.2.0)
|
|
||||||
devise (>= 2.1.0)
|
|
||||||
diff-lcs (1.5.1)
|
|
||||||
dotenv (2.8.1)
|
|
||||||
dotenv-rails (2.8.1)
|
|
||||||
dotenv (= 2.8.1)
|
|
||||||
railties (>= 3.2)
|
|
||||||
drb (2.2.1)
|
|
||||||
e2mmap (0.1.0)
|
|
||||||
email_validator (2.2.4)
|
|
||||||
activemodel
|
|
||||||
erubi (1.13.0)
|
|
||||||
execjs (2.9.1)
|
|
||||||
falcon (0.48.0)
|
|
||||||
async
|
|
||||||
async-container (~> 0.18)
|
|
||||||
async-http (~> 0.75)
|
|
||||||
async-http-cache (~> 0.4)
|
|
||||||
async-service (~> 0.10)
|
|
||||||
bundler
|
|
||||||
localhost (~> 1.1)
|
|
||||||
openssl (~> 3.0)
|
|
||||||
process-metrics (~> 0.2)
|
|
||||||
protocol-http (~> 0.31)
|
|
||||||
protocol-rack (~> 0.7)
|
|
||||||
samovar (~> 2.3)
|
|
||||||
faraday (2.11.0)
|
|
||||||
faraday-net_http (>= 2.0, < 3.4)
|
|
||||||
logger
|
|
||||||
faraday-follow_redirects (0.3.0)
|
|
||||||
faraday (>= 1, < 3)
|
|
||||||
faraday-net_http (3.3.0)
|
|
||||||
net-http
|
|
||||||
ffi (1.17.0)
|
|
||||||
fiber-annotation (0.2.0)
|
|
||||||
fiber-local (1.1.0)
|
|
||||||
fiber-storage
|
|
||||||
fiber-storage (1.0.0)
|
|
||||||
globalid (1.2.1)
|
|
||||||
activesupport (>= 6.1)
|
|
||||||
haml (6.3.0)
|
|
||||||
temple (>= 0.8.2)
|
|
||||||
thor
|
|
||||||
tilt
|
|
||||||
hashie (5.0.0)
|
|
||||||
http_accept_language (2.1.1)
|
|
||||||
httparty (0.22.0)
|
|
||||||
csv
|
|
||||||
mini_mime (>= 1.0.0)
|
|
||||||
multi_xml (>= 0.5.2)
|
|
||||||
i18n (1.14.5)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
io-console (0.7.2)
|
|
||||||
io-endpoint (0.13.1)
|
|
||||||
io-event (1.6.5)
|
|
||||||
io-stream (0.4.0)
|
|
||||||
irb (1.14.0)
|
|
||||||
rdoc (>= 4.0.0)
|
|
||||||
reline (>= 0.4.2)
|
|
||||||
jaro_winkler (1.6.0)
|
|
||||||
jsbundling-rails (1.3.1)
|
|
||||||
railties (>= 6.0.0)
|
|
||||||
json (2.7.2)
|
|
||||||
json-jwt (1.16.6)
|
|
||||||
activesupport (>= 4.2)
|
|
||||||
aes_key_wrap
|
|
||||||
base64
|
|
||||||
bindata
|
|
||||||
faraday (~> 2.0)
|
|
||||||
faraday-follow_redirects
|
|
||||||
kramdown (2.4.0)
|
|
||||||
rexml
|
|
||||||
kramdown-parser-gfm (1.1.0)
|
|
||||||
kramdown (~> 2.0)
|
|
||||||
language_server-protocol (3.17.0.3)
|
|
||||||
launchy (3.0.1)
|
|
||||||
addressable (~> 2.8)
|
|
||||||
childprocess (~> 5.0)
|
|
||||||
letter_opener (1.10.0)
|
|
||||||
launchy (>= 2.2, < 4)
|
|
||||||
localhost (1.3.1)
|
|
||||||
logger (1.6.0)
|
|
||||||
loofah (2.22.0)
|
|
||||||
crass (~> 1.0.2)
|
|
||||||
nokogiri (>= 1.12.0)
|
|
||||||
mail (2.8.1)
|
|
||||||
mini_mime (>= 0.1.1)
|
|
||||||
net-imap
|
|
||||||
net-pop
|
|
||||||
net-smtp
|
|
||||||
mapping (1.1.1)
|
|
||||||
marcel (1.0.4)
|
|
||||||
memory_profiler (1.0.2)
|
|
||||||
metrics (0.10.2)
|
|
||||||
mini_mime (1.1.5)
|
|
||||||
mini_portile2 (2.8.7)
|
|
||||||
minitest (5.25.1)
|
|
||||||
msgpack (1.7.2)
|
|
||||||
multi_xml (0.7.1)
|
|
||||||
bigdecimal (~> 3.1)
|
|
||||||
mysql2 (0.5.6)
|
|
||||||
net-http (0.4.1)
|
|
||||||
uri
|
|
||||||
net-imap (0.4.14)
|
|
||||||
date
|
|
||||||
net-protocol
|
|
||||||
net-pop (0.1.2)
|
|
||||||
net-protocol
|
|
||||||
net-protocol (0.2.2)
|
|
||||||
timeout
|
|
||||||
net-smtp (0.5.0)
|
|
||||||
net-protocol
|
|
||||||
nio4r (2.7.3)
|
|
||||||
nokogiri (1.16.7)
|
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
|
||||||
omniauth (2.1.2)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-rails_csrf_protection (1.0.2)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth_openid_connect (0.7.1)
|
|
||||||
omniauth (>= 1.9, < 3)
|
|
||||||
openid_connect (~> 2.2)
|
|
||||||
openid_connect (2.3.0)
|
|
||||||
activemodel
|
|
||||||
attr_required (>= 1.0.0)
|
|
||||||
email_validator
|
|
||||||
faraday (~> 2.0)
|
|
||||||
faraday-follow_redirects
|
|
||||||
json-jwt (>= 1.16)
|
|
||||||
mail
|
|
||||||
rack-oauth2 (~> 2.2)
|
|
||||||
swd (~> 2.0)
|
|
||||||
tzinfo
|
|
||||||
validate_url
|
|
||||||
webfinger (~> 2.0)
|
|
||||||
openssl (3.2.0)
|
|
||||||
orm_adapter (0.5.0)
|
|
||||||
parallel (1.26.3)
|
|
||||||
parser (3.3.4.2)
|
|
||||||
ast (~> 2.4.1)
|
|
||||||
racc
|
|
||||||
process-metrics (0.3.0)
|
|
||||||
console (~> 1.8)
|
|
||||||
json (~> 2)
|
|
||||||
samovar (~> 2.1)
|
|
||||||
protocol-hpack (1.5.0)
|
|
||||||
protocol-http (0.33.0)
|
|
||||||
protocol-http1 (0.22.0)
|
|
||||||
protocol-http (~> 0.22)
|
|
||||||
protocol-http2 (0.18.0)
|
|
||||||
protocol-hpack (~> 1.4)
|
|
||||||
protocol-http (~> 0.18)
|
|
||||||
protocol-rack (0.7.0)
|
|
||||||
protocol-http (~> 0.27)
|
|
||||||
rack (>= 1.0)
|
|
||||||
psych (5.1.2)
|
|
||||||
stringio
|
|
||||||
public_suffix (6.0.1)
|
|
||||||
racc (1.8.1)
|
|
||||||
rack (3.1.7)
|
|
||||||
rack-attack (6.7.0)
|
|
||||||
rack (>= 1.0, < 4)
|
|
||||||
rack-mini-profiler (3.3.1)
|
|
||||||
rack (>= 1.2.0)
|
|
||||||
rack-oauth2 (2.2.1)
|
|
||||||
activesupport
|
|
||||||
attr_required
|
|
||||||
faraday (~> 2.0)
|
|
||||||
faraday-follow_redirects
|
|
||||||
json-jwt (>= 1.11.0)
|
|
||||||
rack (>= 2.1.0)
|
|
||||||
rack-protection (4.0.0)
|
|
||||||
base64 (>= 0.1.0)
|
|
||||||
rack (>= 3.0.0, < 4)
|
|
||||||
rack-session (2.0.0)
|
|
||||||
rack (>= 3.0.0)
|
|
||||||
rack-test (2.1.0)
|
|
||||||
rack (>= 1.3)
|
|
||||||
rackup (2.1.0)
|
|
||||||
rack (>= 3)
|
|
||||||
webrick (~> 1.8)
|
|
||||||
rails (7.2.1)
|
|
||||||
actioncable (= 7.2.1)
|
|
||||||
actionmailbox (= 7.2.1)
|
|
||||||
actionmailer (= 7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
actiontext (= 7.2.1)
|
|
||||||
actionview (= 7.2.1)
|
|
||||||
activejob (= 7.2.1)
|
|
||||||
activemodel (= 7.2.1)
|
|
||||||
activerecord (= 7.2.1)
|
|
||||||
activestorage (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
bundler (>= 1.15.0)
|
|
||||||
railties (= 7.2.1)
|
|
||||||
rails-dom-testing (2.2.0)
|
|
||||||
activesupport (>= 5.0.0)
|
|
||||||
minitest
|
|
||||||
nokogiri (>= 1.6)
|
|
||||||
rails-html-sanitizer (1.6.0)
|
|
||||||
loofah (~> 2.21)
|
|
||||||
nokogiri (~> 1.14)
|
|
||||||
rails-i18n (7.0.9)
|
|
||||||
i18n (>= 0.7, < 2)
|
|
||||||
railties (>= 6.0.0, < 8)
|
|
||||||
railties (7.2.1)
|
|
||||||
actionpack (= 7.2.1)
|
|
||||||
activesupport (= 7.2.1)
|
|
||||||
irb (~> 1.13)
|
|
||||||
rackup (>= 1.0.0)
|
|
||||||
rake (>= 12.2)
|
|
||||||
thor (~> 1.0, >= 1.2.2)
|
|
||||||
zeitwerk (~> 2.6)
|
|
||||||
rainbow (3.1.1)
|
|
||||||
rake (13.2.1)
|
|
||||||
rbs (2.8.4)
|
|
||||||
rdiscount (2.2.7.3)
|
|
||||||
rdoc (6.7.0)
|
|
||||||
psych (>= 4.0.0)
|
|
||||||
react-rails (2.7.1)
|
|
||||||
babel-transpiler (>= 0.7.0)
|
|
||||||
connection_pool
|
|
||||||
execjs
|
|
||||||
railties (>= 3.2)
|
|
||||||
tilt
|
|
||||||
record_tag_helper (1.0.1)
|
|
||||||
actionview (>= 5)
|
|
||||||
regexp_parser (2.9.2)
|
|
||||||
reline (0.5.9)
|
|
||||||
io-console (~> 0.5)
|
|
||||||
responders (3.1.1)
|
|
||||||
actionpack (>= 5.2)
|
|
||||||
railties (>= 5.2)
|
|
||||||
reverse_markdown (2.1.1)
|
|
||||||
nokogiri
|
|
||||||
rexml (3.3.6)
|
|
||||||
strscan
|
|
||||||
rubocop (1.65.1)
|
|
||||||
json (~> 2.3)
|
|
||||||
language_server-protocol (>= 3.17.0)
|
|
||||||
parallel (~> 1.10)
|
|
||||||
parser (>= 3.3.0.2)
|
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
|
||||||
regexp_parser (>= 2.4, < 3.0)
|
|
||||||
rexml (>= 3.2.5, < 4.0)
|
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
|
||||||
rubocop-ast (1.32.1)
|
|
||||||
parser (>= 3.3.1.0)
|
|
||||||
ruby-progressbar (1.13.0)
|
|
||||||
samovar (2.3.0)
|
|
||||||
console (~> 1.0)
|
|
||||||
mapping (~> 1.0)
|
|
||||||
sanitize (6.1.3)
|
|
||||||
crass (~> 1.0.2)
|
|
||||||
nokogiri (>= 1.12.0)
|
|
||||||
sass-rails (6.0.0)
|
|
||||||
sassc-rails (~> 2.1, >= 2.1.1)
|
|
||||||
sassc (2.4.0)
|
|
||||||
ffi (~> 1.9)
|
|
||||||
sassc-rails (2.1.2)
|
|
||||||
railties (>= 4.0.0)
|
|
||||||
sassc (>= 2.0)
|
|
||||||
sprockets (> 3.0)
|
|
||||||
sprockets-rails
|
|
||||||
tilt
|
|
||||||
securerandom (0.3.1)
|
|
||||||
sentry-rails (5.19.0)
|
|
||||||
railties (>= 5.0)
|
|
||||||
sentry-ruby (~> 5.19.0)
|
|
||||||
sentry-ruby (5.19.0)
|
|
||||||
bigdecimal
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
||||||
shell (0.8.1)
|
|
||||||
e2mmap
|
|
||||||
sync
|
|
||||||
solargraph (0.50.0)
|
|
||||||
backport (~> 1.2)
|
|
||||||
benchmark
|
|
||||||
bundler (~> 2.0)
|
|
||||||
diff-lcs (~> 1.4)
|
|
||||||
e2mmap
|
|
||||||
jaro_winkler (~> 1.5)
|
|
||||||
kramdown (~> 2.3)
|
|
||||||
kramdown-parser-gfm (~> 1.1)
|
|
||||||
parser (~> 3.0)
|
|
||||||
rbs (~> 2.0)
|
|
||||||
reverse_markdown (~> 2.0)
|
|
||||||
rubocop (~> 1.38)
|
|
||||||
thor (~> 1.0)
|
|
||||||
tilt (~> 2.0)
|
|
||||||
yard (~> 0.9, >= 0.9.24)
|
|
||||||
solargraph-rails (1.1.0)
|
|
||||||
activesupport
|
|
||||||
solargraph
|
|
||||||
sprockets (4.2.1)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
rack (>= 2.2.4, < 4)
|
|
||||||
sprockets-rails (3.5.2)
|
|
||||||
actionpack (>= 6.1)
|
|
||||||
activesupport (>= 6.1)
|
|
||||||
sprockets (>= 3.0.0)
|
|
||||||
stackprof (0.2.26)
|
|
||||||
stringio (3.1.1)
|
|
||||||
strscan (3.1.0)
|
|
||||||
swd (2.0.3)
|
|
||||||
activesupport (>= 3)
|
|
||||||
attr_required (>= 0.0.5)
|
|
||||||
faraday (~> 2.0)
|
|
||||||
faraday-follow_redirects
|
|
||||||
sync (0.5.0)
|
|
||||||
temple (0.10.3)
|
|
||||||
terser (1.2.3)
|
|
||||||
execjs (>= 0.3.0, < 3)
|
|
||||||
thor (1.3.1)
|
|
||||||
thread-local (1.1.0)
|
|
||||||
tilt (2.4.0)
|
|
||||||
timeout (0.4.1)
|
|
||||||
traces (0.13.1)
|
|
||||||
turbo-rails (2.0.6)
|
|
||||||
actionpack (>= 6.0.0)
|
|
||||||
activejob (>= 6.0.0)
|
|
||||||
railties (>= 6.0.0)
|
|
||||||
tzinfo (2.0.6)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
unicode-display_width (2.5.0)
|
|
||||||
uri (0.13.1)
|
|
||||||
useragent (0.16.10)
|
|
||||||
validate_url (1.0.15)
|
|
||||||
activemodel (>= 3.0.0)
|
|
||||||
public_suffix
|
|
||||||
warden (1.2.9)
|
|
||||||
rack (>= 2.0.9)
|
|
||||||
web-console (4.2.1)
|
|
||||||
actionview (>= 6.0.0)
|
|
||||||
activemodel (>= 6.0.0)
|
|
||||||
bindex (>= 0.4.0)
|
|
||||||
railties (>= 6.0.0)
|
|
||||||
webfinger (2.1.3)
|
|
||||||
activesupport
|
|
||||||
faraday (~> 2.0)
|
|
||||||
faraday-follow_redirects
|
|
||||||
webrick (1.8.1)
|
|
||||||
websocket-driver (0.7.6)
|
|
||||||
websocket-extensions (>= 0.1.0)
|
|
||||||
websocket-extensions (0.1.5)
|
|
||||||
will_paginate (4.0.1)
|
|
||||||
yard (0.9.36)
|
|
||||||
zeitwerk (2.6.17)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
RocketAMF!
|
|
||||||
addressable (~> 2.8)
|
|
||||||
async (~> 2.17)
|
|
||||||
async-http (~> 0.75.0)
|
|
||||||
bootsnap (~> 1.16)
|
|
||||||
devise (~> 4.9, >= 4.9.2)
|
|
||||||
devise-encryptable (~> 0.2.0)
|
|
||||||
dotenv-rails (~> 2.8, >= 2.8.1)
|
|
||||||
falcon (~> 0.48.0)
|
|
||||||
haml (~> 6.1, >= 6.1.1)
|
|
||||||
http_accept_language (~> 2.1, >= 2.1.1)
|
|
||||||
httparty (~> 0.22.0)
|
|
||||||
jsbundling-rails (~> 1.1)
|
|
||||||
letter_opener (~> 1.8, >= 1.8.1)
|
|
||||||
memory_profiler (~> 1.0)
|
|
||||||
mysql2 (~> 0.5.5)
|
|
||||||
nokogiri (~> 1.15, >= 1.15.3)
|
|
||||||
omniauth (~> 2.1)
|
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
|
||||||
omniauth_openid_connect (~> 0.7.1)
|
|
||||||
parallel (~> 1.23)
|
|
||||||
rack-attack (~> 6.7)
|
|
||||||
rack-mini-profiler (~> 3.1)
|
|
||||||
rails (~> 7.1, >= 7.1.3.4)
|
|
||||||
rails-i18n (~> 7.0, >= 7.0.7)
|
|
||||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
|
||||||
react-rails (~> 2.7, >= 2.7.1)
|
|
||||||
record_tag_helper (~> 1.0, >= 1.0.1)
|
|
||||||
sanitize (~> 6.0, >= 6.0.2)
|
|
||||||
sass-rails (~> 6.0)
|
|
||||||
sentry-rails (~> 5.12)
|
|
||||||
sentry-ruby (~> 5.12)
|
|
||||||
shell (~> 0.8.1)
|
|
||||||
solargraph (~> 0.50.0)
|
|
||||||
solargraph-rails (~> 1.1)
|
|
||||||
sprockets (~> 4.2)
|
|
||||||
stackprof (~> 0.2.25)
|
|
||||||
terser (~> 1.1, >= 1.1.17)
|
|
||||||
thread-local (~> 1.1)
|
|
||||||
turbo-rails (~> 2.0)
|
|
||||||
web-console (~> 4.2)
|
|
||||||
will_paginate (~> 4.0)
|
|
||||||
|
|
||||||
RUBY VERSION
|
|
||||||
ruby 3.3.4p94
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
2.5.18
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Contributor: Matchu
|
Contributor: Matchu
|
||||||
|
|
||||||
Source Code: https://code.openneo.net/OpenNeo/impress
|
Source Code: https://github.com/matchu/impress-2020
|
||||||
|
|
||||||
## Modification
|
## Modification
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
web: unset PORT && env RUBY_DEBUG_OPEN=true bin/rails server
|
|
||||||
js: yarn dev
|
|
||||||
107
README.md
|
|
@ -1,7 +1,108 @@
|
||||||
<img src="https://i.imgur.com/mZ2FCfX.png" width="200" height="200" alt="Dress to Impress beach logo" />
|
<img src="https://i.imgur.com/mZ2FCfX.png" width="200" height="200" alt="Dress to Impress beach logo" />
|
||||||
|
|
||||||
# Dress to Impress
|
# Dress to Impress 2020
|
||||||
|
|
||||||
Oh! We've been revitalizing the Rails app! Fun!
|
This is a rewrite of the Neopets customization app, Dress to Impress!
|
||||||
|
|
||||||
There'll be more to say about it here soon :3
|
It's a React app, using the Next.js framework. (But kinda awkwardly, because it
|
||||||
|
used to be a `create-react-app`, and we never fully rearchitected from that!)
|
||||||
|
|
||||||
|
The motivating goals of the rewrite are:
|
||||||
|
|
||||||
|
- Mobile friendly, to match Neopets's move to mobile.
|
||||||
|
- Simple modern tech, to be more maintainable over time and decrease hosting costs.
|
||||||
|
|
||||||
|
## Installation guide
|
||||||
|
|
||||||
|
### Getting everything set up
|
||||||
|
|
||||||
|
We'll assume you already have your basic development environment ready! Be sure
|
||||||
|
to install the following:
|
||||||
|
|
||||||
|
- Git
|
||||||
|
- Node v16
|
||||||
|
- The Yarn package manager
|
||||||
|
- A MySQL database server
|
||||||
|
|
||||||
|
**Before you clone the repository**, install Git LFS, a tool for managing large
|
||||||
|
files in Git. (We use this for the big batch of public data that we'll import
|
||||||
|
into your dev database.)
|
||||||
|
|
||||||
|
Next, clone this repository, and ensure that
|
||||||
|
`scripts/db/public-data-from-modeling.sql.gz` is around ~30MB large. (If it's
|
||||||
|
much smaller, like 4KB, that probably means Git LFS didn't run correctly, so
|
||||||
|
the next step would be to debug that, delete the repository, and try again!)
|
||||||
|
|
||||||
|
Next, run `yarn install`. This should install the app's NPM dependencies. (You
|
||||||
|
may need to install some additional libraries to your machine for certain
|
||||||
|
dependencies to install correctly. See the instructions for
|
||||||
|
[canvas][npm-canvas] in particular!)
|
||||||
|
|
||||||
|
### Create your development database
|
||||||
|
|
||||||
|
Next, create two MySQL databases: `openneo_impress` and `openneo_id`. Then,
|
||||||
|
create a MySQL user named `impress_dev` with password `impress_dev`,
|
||||||
|
with full permissions for both databases.
|
||||||
|
|
||||||
|
(We're assuming that, on your local machine, your MySQL server isn't connected
|
||||||
|
to the outside internet, and that there probably won't be sensitive information
|
||||||
|
stored in your DTI database anyway, so it should be okay for this username and
|
||||||
|
password to be hardcoded.)
|
||||||
|
|
||||||
|
Finally, run `yarn db:setup-dev:full` to fill the databases
|
||||||
|
with the necessary schema, plus some real public data exported from DTI—like
|
||||||
|
items, species, and colors!
|
||||||
|
|
||||||
|
### See it work!
|
||||||
|
|
||||||
|
Okay, let's run `yarn dev`! This should start a DTI server on port 3000. Open
|
||||||
|
it in your browser and hopefully it works!! 🤞
|
||||||
|
|
||||||
|
### Optional: You might need some environment variables
|
||||||
|
|
||||||
|
In Next.js, you can set environment variables in a `.env` file, in the root of
|
||||||
|
the app. (This will be ignored by Git, thanks to our `.gitignore` file.)
|
||||||
|
|
||||||
|
Note that some the features of the site won't work without special environment
|
||||||
|
variables set, because they depend on production services we can't reproduce
|
||||||
|
locally. But they generally fail gracefully and show a helpful error message,
|
||||||
|
so you mostly won't have to worry about it until you run into it!
|
||||||
|
|
||||||
|
You mostly won't need to use this! But one early case you'll run into: for
|
||||||
|
account creation and login to work, you'll need to create a `.env` file with a
|
||||||
|
value for `DTI_AUTH_TOKEN_SECRET`: a secret string we use to cryptographically
|
||||||
|
validates the user's login cookie. In production this is a closely-guarded
|
||||||
|
secret, but for development, just open a random password generator and
|
||||||
|
copy-paste the result into `.env`!
|
||||||
|
|
||||||
|
```
|
||||||
|
DTI_AUTH_TOKEN_SECRET=jl2DFjkewkrufsIDKwhatever
|
||||||
|
```
|
||||||
|
|
||||||
|
[npm-canvas]: https://www.npmjs.com/package/canvas
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
First, there's the core app, in this repository.
|
||||||
|
|
||||||
|
- **React app:** Runs mainly on the client's machine. Code in `src/app`.
|
||||||
|
- **API functions:** Run on our VPS server. Code in `api` and `src/server`.
|
||||||
|
|
||||||
|
Then, there's our various data storage components.
|
||||||
|
|
||||||
|
- **MySQL database:** Runs on our Linode VPS, colocated with the old app.
|
||||||
|
- **Amazon S3:** Stores PNGs of pet/item appearance layers, converted from the Neopets SWFs. _(Once Neopets releases HTML5-compatible assets for all their items, we can hopefully remove this!)_
|
||||||
|
|
||||||
|
Finally, there's our third-party integrations.
|
||||||
|
|
||||||
|
- **Honeycomb:** For observability & performance insights on the backend.
|
||||||
|
- **Discord:** For logging Support users' actions to a private Discord server.
|
||||||
|
- **Neopets:** We load pet data from them! And plenty of assets!
|
||||||
|
- **Fastly:** A CDN cache that sits in front of our server to help cache common requests and expensive operations. We also use them to proxy for `images.neopets.com` in some cases, so we can add crossdomain headers.
|
||||||
|
|
||||||
|
Notable old components _not_ currently included in Impress 2020:
|
||||||
|
|
||||||
|
- **Elasticsearch:** Used for lightning-fast item search queries. So far, we're finding the MySQL queries to be fast enough in practice. Might consider using some kind of fulltext query engine if that doesn't scale with more users!
|
||||||
|
- **Resque:** Used to schedule background tasks for modeling and outfit thumbnails. (We now perform these tasks on-demand, and use Fastly to cache things like thumbnails!)
|
||||||
|
- **Memcache:** Used to cache common HTML and JSON snippets. Not yet needing anything similar in Impress 2020!
|
||||||
|
- **The entire old Rails app!** No references to it in here, aside from some temporary URL links to features that aren't implemented here yet.
|
||||||
|
|
|
||||||
11
Rakefile
|
|
@ -1,11 +0,0 @@
|
||||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
|
||||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
|
||||||
|
|
||||||
require File.expand_path('../config/application', __FILE__)
|
|
||||||
|
|
||||||
require 'rake'
|
|
||||||
require 'rake/testtask'
|
|
||||||
require 'rdoc/task'
|
|
||||||
|
|
||||||
OpenneoImpressItems::Application.load_tasks
|
|
||||||
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
//= link_tree ../images
|
|
||||||
//= link_tree ../javascripts .js
|
|
||||||
//= link_tree ../stylesheets .css
|
|
||||||
//= link_directory ../fonts .otf
|
|
||||||
//= link_tree ../builds
|
|
||||||
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 957 B |
|
Before Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 801 B |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 516 B |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 655 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 732 B |
|
Before Width: | Height: | Size: 754 B |
|
Before Width: | Height: | Size: 756 B |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 537 B |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 671 B |
|
Before Width: | Height: | Size: 5.2 KiB |
|
|
@ -1,20 +0,0 @@
|
||||||
(function () {
|
|
||||||
var CSRFProtection;
|
|
||||||
var token = $('meta[name="csrf-token"]').attr("content");
|
|
||||||
if (token) {
|
|
||||||
CSRFProtection = function (xhr, settings) {
|
|
||||||
var sendToken =
|
|
||||||
typeof settings.useCSRFProtection === "undefined" || // default to true
|
|
||||||
settings.useCSRFProtection;
|
|
||||||
if (sendToken) {
|
|
||||||
xhr.setRequestHeader("X-CSRF-Token", token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
CSRFProtection = $.noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajaxSetup({
|
|
||||||
beforeSend: CSRFProtection,
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,842 +0,0 @@
|
||||||
(function () {
|
|
||||||
var hangersInitCallbacks = [];
|
|
||||||
|
|
||||||
function onHangersInit(callback) {
|
|
||||||
hangersInitCallbacks[hangersInitCallbacks.length] = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hangersInit() {
|
|
||||||
for (var i = 0; i < hangersInitCallbacks.length; i++) {
|
|
||||||
try {
|
|
||||||
hangersInitCallbacks[i]();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Hanger groups
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
var hangerGroups = [];
|
|
||||||
|
|
||||||
$(".closet-hangers-group").each(function () {
|
|
||||||
var el = $(this);
|
|
||||||
var lists = [];
|
|
||||||
|
|
||||||
el.find("div.closet-list").each(function () {
|
|
||||||
var el = $(this);
|
|
||||||
var id = el.attr("data-id");
|
|
||||||
if (id) {
|
|
||||||
lists[lists.length] = {
|
|
||||||
id: parseInt(id, 10),
|
|
||||||
label: el.find("h4").text(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hangerGroups[hangerGroups.length] = {
|
|
||||||
label: el.find("h3").text(),
|
|
||||||
lists: lists,
|
|
||||||
owned: el.attr("data-owned") == "true",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".closet-hangers-group span.toggle").live("click", function () {
|
|
||||||
$(this).closest(".closet-hangers-group").toggleClass("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
var hangersElQuery = "#closet-hangers";
|
|
||||||
var hangersEl = $(hangersElQuery);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Compare with Your Items
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
$("#toggle-compare").click(function () {
|
|
||||||
hangersEl.toggleClass("comparing");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read the item IDs of trade matches from the meta tags.
|
|
||||||
const ownedIds =
|
|
||||||
document
|
|
||||||
.querySelector("meta[name=trade-matches-owns]")
|
|
||||||
?.getAttribute("value")
|
|
||||||
?.split(",") ?? [];
|
|
||||||
const wantedIds =
|
|
||||||
document
|
|
||||||
.querySelector("meta[name=trade-matches-wants]")
|
|
||||||
?.getAttribute("value")
|
|
||||||
?.split(",") ?? [];
|
|
||||||
|
|
||||||
// Apply the `user-owns` and `user-wants` classes to the relevant entries.
|
|
||||||
// This both provides immediate visual feedback, and sets up "Compare with
|
|
||||||
// Your Items" to toggle to just them!
|
|
||||||
//
|
|
||||||
// NOTE: The motivation here is caching: this allows us to share a cache of
|
|
||||||
// the closet list contents across all users, without `user-owns` or
|
|
||||||
// `user-wants` classes for one specific user getting cached and reused.
|
|
||||||
const hangerEls = document.querySelectorAll("#closet-hangers .object");
|
|
||||||
for (const hangerEl of hangerEls) {
|
|
||||||
const itemId = hangerEl.getAttribute("data-item-id");
|
|
||||||
if (ownedIds.includes(itemId)) {
|
|
||||||
hangerEl.classList.add("user-owns");
|
|
||||||
}
|
|
||||||
if (wantedIds.includes(itemId)) {
|
|
||||||
hangerEl.classList.add("user-wants");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Hanger forms
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
var body = $(document.body).addClass("js");
|
|
||||||
if (!body.hasClass("current-user")) return false;
|
|
||||||
|
|
||||||
// When we get hangers HTML, add the controls. We do this in JS rather than
|
|
||||||
// in the HTML for caching, since otherwise the requests can take forever.
|
|
||||||
// If there were another way to add hangers, then we'd have to worry about
|
|
||||||
// that, but, right now, the only way to create a new hanger from this page
|
|
||||||
// is through the autocompleter, which reinitializes anyway. Geez, this thing
|
|
||||||
// is begging for a rewrite, but today we're here for performance.
|
|
||||||
$("#closet-hanger-update-tmpl").template("updateFormTmpl");
|
|
||||||
$("#closet-hanger-destroy-tmpl").template("destroyFormTmpl");
|
|
||||||
onHangersInit(function () {
|
|
||||||
// Super-lame hack to get the user ID from where it already is :/
|
|
||||||
var currentUserId = itemsSearchForm.data("current-user-id");
|
|
||||||
$("#closet-hangers .closet-hangers-group").each(function () {
|
|
||||||
var groupEl = $(this);
|
|
||||||
var owned = groupEl.data("owned");
|
|
||||||
|
|
||||||
groupEl.find("div.closet-list").each(function () {
|
|
||||||
var listEl = $(this);
|
|
||||||
var listId = listEl.data("id");
|
|
||||||
|
|
||||||
listEl.find("div.object").each(function () {
|
|
||||||
var hangerEl = $(this);
|
|
||||||
var hangerId = hangerEl.data("id");
|
|
||||||
var quantityEl = hangerEl.find("div.quantity");
|
|
||||||
var quantity = hangerEl.data("quantity");
|
|
||||||
|
|
||||||
// Ooh, this part is weird. We only want the name to be linked, so
|
|
||||||
// lift everything else out.
|
|
||||||
var checkboxId = "hanger-selected-" + hangerId;
|
|
||||||
var label = $("<label />", { for: checkboxId });
|
|
||||||
var link = hangerEl.children("a");
|
|
||||||
link.children(":not(.name)").detach().appendTo(label);
|
|
||||||
link.detach().appendTo(label);
|
|
||||||
var checkbox = $("<input />", {
|
|
||||||
type: "checkbox",
|
|
||||||
id: checkboxId,
|
|
||||||
}).appendTo(hangerEl);
|
|
||||||
label.appendTo(hangerEl);
|
|
||||||
|
|
||||||
// I don't usually like to _blank things, but it's too easy to click
|
|
||||||
// the text when you didn't mean to and lose your selection work.
|
|
||||||
link.attr("target", "_blank");
|
|
||||||
|
|
||||||
$.tmpl("updateFormTmpl", {
|
|
||||||
user_id: currentUserId,
|
|
||||||
closet_hanger_id: hangerId,
|
|
||||||
quantity: quantity,
|
|
||||||
list_id: listId,
|
|
||||||
owned: owned,
|
|
||||||
}).appendTo(quantityEl);
|
|
||||||
|
|
||||||
$.tmpl("destroyFormTmpl", {
|
|
||||||
user_id: currentUserId,
|
|
||||||
closet_hanger_id: hangerId,
|
|
||||||
}).appendTo(hangerEl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$.fn.liveDraggable = function (opts) {
|
|
||||||
this.live("mouseover", function () {
|
|
||||||
if (!$(this).data("init")) {
|
|
||||||
$(this).data("init", true).draggable(opts);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.disableForms = function () {
|
|
||||||
return this.data("formsDisabled", true)
|
|
||||||
.find("input")
|
|
||||||
.attr("disabled", "disabled")
|
|
||||||
.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.enableForms = function () {
|
|
||||||
return this.data("formsDisabled", false)
|
|
||||||
.find("input")
|
|
||||||
.removeAttr("disabled")
|
|
||||||
.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.hasChanged = function () {
|
|
||||||
return this.attr("data-previous-value") != this.val();
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.revertValue = function () {
|
|
||||||
return this.each(function () {
|
|
||||||
var el = $(this);
|
|
||||||
el.val(el.attr("data-previous-value"));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.storeValue = function () {
|
|
||||||
return this.each(function () {
|
|
||||||
var el = $(this);
|
|
||||||
el.attr("data-previous-value", el.val());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.insertIntoSortedList = function (list, compare) {
|
|
||||||
var newChild = this,
|
|
||||||
inserted = false;
|
|
||||||
list.children().each(function () {
|
|
||||||
if (compare(newChild, $(this)) < 1) {
|
|
||||||
newChild.insertBefore(this);
|
|
||||||
inserted = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!inserted) newChild.appendTo(list);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSaveError(xhr, action) {
|
|
||||||
try {
|
|
||||||
var data = $.parseJSON(xhr.responseText);
|
|
||||||
} catch (e) {
|
|
||||||
var data = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.errors != "undefined") {
|
|
||||||
$.jGrowl("Error " + action + ": " + data.errors.join(", "));
|
|
||||||
} else {
|
|
||||||
$.jGrowl("We had trouble " + action + " just now. Try again?");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectRemoved(objectWrapper) {
|
|
||||||
objectWrapper.hide(250, function () {
|
|
||||||
objectWrapper.remove();
|
|
||||||
updateBulkActions();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareItemsByName(a, b) {
|
|
||||||
return a.find("span.name").text().localeCompare(b.find("span.name").text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function findList(owned, id, item) {
|
|
||||||
if (id) {
|
|
||||||
return $("#closet-list-" + id);
|
|
||||||
} else {
|
|
||||||
return $(
|
|
||||||
".closet-hangers-group[data-owned=" +
|
|
||||||
owned +
|
|
||||||
"] div.closet-list.unlisted",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateListHangersCount(el) {
|
|
||||||
el.attr("data-hangers-count", el.find("div.object").length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveItemToList(item, owned, listId) {
|
|
||||||
var newList = findList(owned, listId, item);
|
|
||||||
var oldList = item.closest("div.closet-list");
|
|
||||||
var hangersWrapper = newList.find("div.closet-list-hangers");
|
|
||||||
item.insertIntoSortedList(hangersWrapper, compareItemsByName);
|
|
||||||
updateListHangersCount(oldList);
|
|
||||||
updateListHangersCount(newList);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitUpdateForm(form) {
|
|
||||||
if (form.data("loading")) return false;
|
|
||||||
var quantityEl = form.children("input[name=closet_hanger[quantity]]");
|
|
||||||
var ownedEl = form.children("input[name=closet_hanger[owned]]");
|
|
||||||
var listEl = form.children("input[name=closet_hanger[list_id]]");
|
|
||||||
var listChanged = ownedEl.hasChanged() || listEl.hasChanged();
|
|
||||||
if (listChanged || quantityEl.hasChanged()) {
|
|
||||||
var objectWrapper = form.closest(".object").addClass("loading");
|
|
||||||
var newQuantity = quantityEl.val();
|
|
||||||
var quantitySpan = objectWrapper.find(".quantity span").text(newQuantity);
|
|
||||||
objectWrapper.attr("data-quantity", newQuantity);
|
|
||||||
var data = form.serialize(); // get data before disabling inputs
|
|
||||||
objectWrapper.disableForms();
|
|
||||||
form.data("loading", true);
|
|
||||||
if (listChanged)
|
|
||||||
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
|
|
||||||
$.ajax({
|
|
||||||
url: form.attr("action") + ".json",
|
|
||||||
type: "post",
|
|
||||||
data: data,
|
|
||||||
dataType: "json",
|
|
||||||
complete: function (data) {
|
|
||||||
if (quantityEl.val() == 0) {
|
|
||||||
objectRemoved(objectWrapper);
|
|
||||||
} else {
|
|
||||||
objectWrapper.removeClass("loading").enableForms();
|
|
||||||
}
|
|
||||||
form.data("loading", false);
|
|
||||||
},
|
|
||||||
success: function () {
|
|
||||||
// Now that the move was successful, let's merge it with any
|
|
||||||
// conflicting hangers
|
|
||||||
var id = objectWrapper.attr("data-item-id");
|
|
||||||
var conflictingHanger = findList(
|
|
||||||
ownedEl.val(),
|
|
||||||
listEl.val(),
|
|
||||||
objectWrapper,
|
|
||||||
)
|
|
||||||
.find("div[data-item-id=" + id + "]")
|
|
||||||
.not(objectWrapper);
|
|
||||||
if (conflictingHanger.length) {
|
|
||||||
var conflictingQuantity = parseInt(
|
|
||||||
conflictingHanger.attr("data-quantity"),
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
var currentQuantity = parseInt(newQuantity, 10);
|
|
||||||
|
|
||||||
var mergedQuantity = conflictingQuantity + currentQuantity;
|
|
||||||
|
|
||||||
quantitySpan.text(mergedQuantity);
|
|
||||||
quantityEl.val(mergedQuantity);
|
|
||||||
objectWrapper.attr("data-quantity", mergedQuantity);
|
|
||||||
|
|
||||||
conflictingHanger.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
quantityEl.storeValue();
|
|
||||||
ownedEl.storeValue();
|
|
||||||
listEl.storeValue();
|
|
||||||
|
|
||||||
updateBulkActions();
|
|
||||||
},
|
|
||||||
error: function (xhr) {
|
|
||||||
quantityEl.revertValue();
|
|
||||||
ownedEl.revertValue();
|
|
||||||
listEl.revertValue();
|
|
||||||
if (listChanged)
|
|
||||||
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
|
|
||||||
quantitySpan.text(quantityEl.val());
|
|
||||||
|
|
||||||
handleSaveError(xhr, "updating the quantity");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(hangersElQuery + " form.closet-hanger-update").live("submit", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
submitUpdateForm($(this));
|
|
||||||
});
|
|
||||||
|
|
||||||
function editableInputs() {
|
|
||||||
return $(hangersElQuery).find(
|
|
||||||
"input[name=closet_hanger[quantity]], " +
|
|
||||||
"input[name=closet_hanger[owned]], " +
|
|
||||||
"input[name=closet_hanger[list_id]]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(hangersElQuery + "input[name=closet_hanger[quantity]]")
|
|
||||||
.live("change", function () {
|
|
||||||
submitUpdateForm($(this).parent());
|
|
||||||
})
|
|
||||||
.storeValue();
|
|
||||||
|
|
||||||
onHangersInit(function () {
|
|
||||||
editableInputs().storeValue();
|
|
||||||
});
|
|
||||||
|
|
||||||
$(hangersElQuery + " div.object")
|
|
||||||
.live("mouseleave", function () {
|
|
||||||
submitUpdateForm($(this).find("form.closet-hanger-update"));
|
|
||||||
})
|
|
||||||
.liveDraggable({
|
|
||||||
appendTo: "#closet-hangers",
|
|
||||||
distance: 20,
|
|
||||||
helper: "clone",
|
|
||||||
revert: "invalid",
|
|
||||||
});
|
|
||||||
|
|
||||||
$(hangersElQuery + " form.closet-hanger-destroy").live(
|
|
||||||
"submit",
|
|
||||||
function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var form = $(this);
|
|
||||||
var button = form.children("input[type=submit]").val("Removing…");
|
|
||||||
var objectWrapper = form.closest(".object").addClass("loading");
|
|
||||||
var data = form.serialize(); // get data before disabling inputs
|
|
||||||
objectWrapper.addClass("loading").disableForms();
|
|
||||||
$.ajax({
|
|
||||||
url: form.attr("action") + ".json",
|
|
||||||
type: "post",
|
|
||||||
data: data,
|
|
||||||
dataType: "json",
|
|
||||||
complete: function () {
|
|
||||||
button.val("Remove");
|
|
||||||
},
|
|
||||||
success: function () {
|
|
||||||
objectRemoved(objectWrapper);
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
objectWrapper.removeClass("loading").enableForms();
|
|
||||||
$.jGrowl("Error removing item. Try again?");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
$(hangersElQuery + " .select-all").live("click", function (e) {
|
|
||||||
var checkboxes = $(this)
|
|
||||||
.closest(".closet-list")
|
|
||||||
.find(".object input[type=checkbox]");
|
|
||||||
|
|
||||||
var allChecked = true;
|
|
||||||
checkboxes.each(function () {
|
|
||||||
if (!this.checked) {
|
|
||||||
allChecked = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
checkboxes.attr("checked", !allChecked);
|
|
||||||
|
|
||||||
updateBulkActions(); // setting the checked prop doesn't fire change events
|
|
||||||
});
|
|
||||||
|
|
||||||
function getCheckboxes() {
|
|
||||||
return $(hangersElQuery + " input[type=checkbox]");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCheckedIds() {
|
|
||||||
var checkedIds = [];
|
|
||||||
getCheckboxes()
|
|
||||||
.filter(":checked")
|
|
||||||
.each(function () {
|
|
||||||
if (this.checked) checkedIds.push(this.id);
|
|
||||||
});
|
|
||||||
return checkedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCheckboxes().live("change", updateBulkActions);
|
|
||||||
|
|
||||||
function updateBulkActions() {
|
|
||||||
var checkedCount = getCheckboxes().filter(":checked").length;
|
|
||||||
$(".bulk-actions").attr("data-target-count", checkedCount);
|
|
||||||
$(".bulk-actions-target-count").text(checkedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(".bulk-actions-move-all").bind("submit", function (e) {
|
|
||||||
// TODO: DRY
|
|
||||||
e.preventDefault();
|
|
||||||
var form = $(this);
|
|
||||||
var data = form.serializeArray();
|
|
||||||
data.push({
|
|
||||||
name: "return_to",
|
|
||||||
value: window.location.pathname + window.location.search,
|
|
||||||
});
|
|
||||||
|
|
||||||
var checkedBoxes = getCheckboxes().filter(":checked");
|
|
||||||
checkedBoxes.each(function () {
|
|
||||||
data.push({
|
|
||||||
name: "ids[]",
|
|
||||||
value: $(this).closest(".object").attr("data-id"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: form.attr("action"),
|
|
||||||
type: form.attr("method"),
|
|
||||||
data: data,
|
|
||||||
success: function (html) {
|
|
||||||
var doc = $(html);
|
|
||||||
hangersEl.html(doc.find("#closet-hangers").html());
|
|
||||||
hangersInit();
|
|
||||||
updateBulkActions(); // don't want to maintain checked; deselect em all
|
|
||||||
doc
|
|
||||||
.find(".flash")
|
|
||||||
.hide()
|
|
||||||
.insertBefore(hangersEl)
|
|
||||||
.show(500)
|
|
||||||
.delay(5000)
|
|
||||||
.hide(250);
|
|
||||||
itemsSearchField.val("");
|
|
||||||
},
|
|
||||||
error: function (xhr) {
|
|
||||||
handleSaveError(xhr, "moving these items");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".bulk-actions-remove-all").bind("submit", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var form = $(this);
|
|
||||||
var hangerIds = [];
|
|
||||||
var checkedBoxes = getCheckboxes().filter(":checked");
|
|
||||||
var hangerEls = $();
|
|
||||||
checkedBoxes.each(function () {
|
|
||||||
hangerEls = hangerEls.add($(this).closest(".object"));
|
|
||||||
});
|
|
||||||
hangerEls.each(function () {
|
|
||||||
hangerIds.push($(this).attr("data-id"));
|
|
||||||
});
|
|
||||||
$.ajax({
|
|
||||||
url: form.attr("action") + ".json?" + $.param({ ids: hangerIds }),
|
|
||||||
type: "delete",
|
|
||||||
dataType: "json",
|
|
||||||
success: function () {
|
|
||||||
objectRemoved(hangerEls);
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
$.jGrowl("Error removing items. Try again?");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".bulk-actions-deselect-all").bind("click", function (e) {
|
|
||||||
getCheckboxes().filter(":checked").attr("checked", false);
|
|
||||||
updateBulkActions();
|
|
||||||
});
|
|
||||||
|
|
||||||
function maintainCheckboxes(fn) {
|
|
||||||
var checkedIds = getCheckedIds();
|
|
||||||
|
|
||||||
fn();
|
|
||||||
|
|
||||||
checkedIds.forEach(function (id) {
|
|
||||||
document.getElementById(id).checked = true;
|
|
||||||
});
|
|
||||||
updateBulkActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Search, autocomplete
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
var itemsSearchForm = $("#closet-hangers-items-search[data-current-user-id]");
|
|
||||||
var itemsSearchField = itemsSearchForm.children("input[name=q]");
|
|
||||||
|
|
||||||
itemsSearchField.autocomplete({
|
|
||||||
select: function (e, ui) {
|
|
||||||
if (ui.item.is_item) {
|
|
||||||
// Let the autocompleter finish up this search before starting a new one
|
|
||||||
setTimeout(function () {
|
|
||||||
itemsSearchField.autocomplete("search", ui.item);
|
|
||||||
}, 0);
|
|
||||||
} else {
|
|
||||||
var item = ui.item.item;
|
|
||||||
var group = ui.item.group;
|
|
||||||
|
|
||||||
itemsSearchField.addClass("loading");
|
|
||||||
|
|
||||||
var closetHanger = {
|
|
||||||
owned: group.owned,
|
|
||||||
list_id: ui.item.list ? ui.item.list.id : "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!item.hasHanger) closetHanger.quantity = 1;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url:
|
|
||||||
"/user/" +
|
|
||||||
itemsSearchForm.data("current-user-id") +
|
|
||||||
"/items/" +
|
|
||||||
item.id +
|
|
||||||
"/closet_hangers",
|
|
||||||
type: "post",
|
|
||||||
data: {
|
|
||||||
closet_hanger: closetHanger,
|
|
||||||
return_to: window.location.pathname + window.location.search,
|
|
||||||
},
|
|
||||||
complete: function () {
|
|
||||||
itemsSearchField.removeClass("loading");
|
|
||||||
},
|
|
||||||
success: function (html) {
|
|
||||||
var doc = $(html);
|
|
||||||
maintainCheckboxes(function () {
|
|
||||||
hangersEl.html(doc.find("#closet-hangers").html());
|
|
||||||
hangersInit();
|
|
||||||
});
|
|
||||||
doc
|
|
||||||
.find(".flash")
|
|
||||||
.hide()
|
|
||||||
.insertBefore(hangersEl)
|
|
||||||
.show(500)
|
|
||||||
.delay(5000)
|
|
||||||
.hide(250);
|
|
||||||
itemsSearchField.val("");
|
|
||||||
},
|
|
||||||
error: function (xhr) {
|
|
||||||
handleSaveError(xhr, "adding the item");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
source: function (input, callback) {
|
|
||||||
if (typeof input.term == "string") {
|
|
||||||
// user-typed query
|
|
||||||
$.getJSON("/items.json?q=" + input.term, function (data) {
|
|
||||||
var output = [];
|
|
||||||
var items = data.items;
|
|
||||||
for (var i in items) {
|
|
||||||
items[i].label = items[i].name;
|
|
||||||
items[i].is_item = true;
|
|
||||||
output[output.length] = items[i];
|
|
||||||
}
|
|
||||||
callback(output);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// item was chosen, now choose a group to insert
|
|
||||||
var groupInserts = [],
|
|
||||||
group;
|
|
||||||
var item = input.term,
|
|
||||||
itemEl,
|
|
||||||
occupiedGroups,
|
|
||||||
hasHanger;
|
|
||||||
for (var i in hangerGroups) {
|
|
||||||
group = hangerGroups[i];
|
|
||||||
itemEl = $(
|
|
||||||
".closet-hangers-group[data-owned=" +
|
|
||||||
group.owned +
|
|
||||||
"] div.object[data-item-id=" +
|
|
||||||
item.id +
|
|
||||||
"]",
|
|
||||||
);
|
|
||||||
occupiedGroups = itemEl.closest(".closet-list");
|
|
||||||
hasHanger = occupiedGroups.filter(".unlisted").length > 0;
|
|
||||||
|
|
||||||
groupInserts[groupInserts.length] = {
|
|
||||||
group: group,
|
|
||||||
item: item,
|
|
||||||
label: item.label,
|
|
||||||
hasHanger: hasHanger,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var i = 0; i < group.lists.length; i++) {
|
|
||||||
hasHanger =
|
|
||||||
occupiedGroups.filter("[data-id=" + group.lists[i].id + "]")
|
|
||||||
.length > 0;
|
|
||||||
groupInserts[groupInserts.length] = {
|
|
||||||
group: group,
|
|
||||||
item: item,
|
|
||||||
label: item.label,
|
|
||||||
list: group.lists[i],
|
|
||||||
hasHanger: hasHanger,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback(groupInserts);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var autocompleter = itemsSearchField.data("autocomplete");
|
|
||||||
|
|
||||||
autocompleter._renderItem = function (ul, item) {
|
|
||||||
var li = $("<li></li>").data("item.autocomplete", item);
|
|
||||||
if (item.is_item) {
|
|
||||||
// these are items from the server
|
|
||||||
$("#autocomplete-item-tmpl").tmpl({ item_name: item.label }).appendTo(li);
|
|
||||||
} else if (item.list) {
|
|
||||||
// these are list inserts
|
|
||||||
var listName = item.list.label;
|
|
||||||
if (item.hasHanger) {
|
|
||||||
$("#autocomplete-already-in-collection-tmpl")
|
|
||||||
.tmpl({ collection_name: listName })
|
|
||||||
.appendTo(li);
|
|
||||||
} else {
|
|
||||||
$("#autocomplete-add-to-list-tmpl")
|
|
||||||
.tmpl({ list_name: listName })
|
|
||||||
.appendTo(li);
|
|
||||||
}
|
|
||||||
li.addClass("closet-list-autocomplete-item");
|
|
||||||
} else {
|
|
||||||
// these are group inserts
|
|
||||||
var groupName = item.group.label;
|
|
||||||
if (!item.hasHanger) {
|
|
||||||
$("#autocomplete-add-to-group-tmpl")
|
|
||||||
.tmpl({ group_name: groupName.replace(/\s+$/, "") })
|
|
||||||
.appendTo(li);
|
|
||||||
} else {
|
|
||||||
$("#autocomplete-already-in-collection-tmpl")
|
|
||||||
.tmpl({ collection_name: groupName })
|
|
||||||
.appendTo(li);
|
|
||||||
}
|
|
||||||
li.addClass("closet-hangers-group-autocomplete-item");
|
|
||||||
}
|
|
||||||
return li.appendTo(ul);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Contact Neopets username form
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
var contactEl = $("#closet-hangers-contact");
|
|
||||||
var contactForm = contactEl.children("form");
|
|
||||||
var contactField = contactForm.children("select");
|
|
||||||
|
|
||||||
var contactAddOption = $("<option/>", {
|
|
||||||
text: contactField.attr("data-new-text"),
|
|
||||||
value: -1,
|
|
||||||
});
|
|
||||||
contactAddOption.appendTo(contactField);
|
|
||||||
var currentUserId = $("meta[name=current-user-id]").attr("content");
|
|
||||||
|
|
||||||
function submitContactForm() {
|
|
||||||
var data = contactForm.serialize();
|
|
||||||
contactForm.disableForms();
|
|
||||||
$.ajax({
|
|
||||||
url: contactForm.attr("action") + ".json",
|
|
||||||
type: "post",
|
|
||||||
data: data,
|
|
||||||
dataType: "json",
|
|
||||||
complete: function () {
|
|
||||||
contactForm.enableForms();
|
|
||||||
},
|
|
||||||
error: function (xhr) {
|
|
||||||
handleSaveError(xhr, "saving Neopets username");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
contactField.change(function (e) {
|
|
||||||
if (contactField.val() < 0) {
|
|
||||||
var newUsername = $.trim(
|
|
||||||
prompt(contactField.attr("data-new-prompt"), ""),
|
|
||||||
);
|
|
||||||
if (newUsername) {
|
|
||||||
$.ajax({
|
|
||||||
url: "/user/" + currentUserId + "/neopets-connections",
|
|
||||||
type: "POST",
|
|
||||||
data: { neopets_connection: { neopets_username: newUsername } },
|
|
||||||
dataType: "json",
|
|
||||||
success: function (connection) {
|
|
||||||
var newOption = $("<option/>", {
|
|
||||||
text: newUsername,
|
|
||||||
value: connection.id,
|
|
||||||
});
|
|
||||||
newOption.insertBefore(contactAddOption);
|
|
||||||
contactField.val(connection.id);
|
|
||||||
submitContactForm();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
submitContactForm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Closet list droppable
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
onHangersInit(function () {
|
|
||||||
$("div.closet-list").droppable({
|
|
||||||
accept: "div.object",
|
|
||||||
activate: function () {
|
|
||||||
$(this)
|
|
||||||
.find(".closet-list-content")
|
|
||||||
.animate({ opacity: 0, height: 100 }, 250);
|
|
||||||
},
|
|
||||||
activeClass: "droppable-active",
|
|
||||||
deactivate: function () {
|
|
||||||
$(this)
|
|
||||||
.find(".closet-list-content")
|
|
||||||
.css("height", "auto")
|
|
||||||
.animate({ opacity: 1 }, 250);
|
|
||||||
},
|
|
||||||
drop: function (e, ui) {
|
|
||||||
var form = ui.draggable.find("form.closet-hanger-update");
|
|
||||||
form
|
|
||||||
.find("input[name=closet_hanger[list_id]]")
|
|
||||||
.val(this.getAttribute("data-id"));
|
|
||||||
form
|
|
||||||
.find("input[name=closet_hanger[owned]]")
|
|
||||||
.val($(this).closest(".closet-hangers-group").attr("data-owned"));
|
|
||||||
submitUpdateForm(form);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Visibility Descriptions
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updateVisibilityDescription() {
|
|
||||||
var descriptions = $(this)
|
|
||||||
.closest(".visibility-form")
|
|
||||||
.find("ul.visibility-descriptions");
|
|
||||||
|
|
||||||
descriptions.children("li.current").removeClass("current");
|
|
||||||
descriptions
|
|
||||||
.children("li[data-id=" + $(this).val() + "]")
|
|
||||||
.addClass("current");
|
|
||||||
}
|
|
||||||
|
|
||||||
function visibilitySelects() {
|
|
||||||
return $("form.visibility-form select");
|
|
||||||
}
|
|
||||||
|
|
||||||
visibilitySelects().live("change", updateVisibilityDescription);
|
|
||||||
|
|
||||||
onHangersInit(function () {
|
|
||||||
visibilitySelects().each(updateVisibilityDescription);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Help
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
$("#toggle-help").click(function () {
|
|
||||||
$("#closet-hangers-help").toggleClass("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Share URL
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
$("#closet-hangers-share-box")
|
|
||||||
.mouseover(function () {
|
|
||||||
$(this).focus();
|
|
||||||
})
|
|
||||||
.mouseout(function () {
|
|
||||||
$(this).blur();
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Initialize
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
hangersInit();
|
|
||||||
})();
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
(function () {
|
|
||||||
function setChecked() {
|
|
||||||
var el = $(this);
|
|
||||||
el.closest("li").toggleClass("checked", el.is(":checked"));
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#petpage-closet-lists input").click(setChecked).each(setChecked);
|
|
||||||
})();
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
document.addEventListener("change", ({ target }) => {
|
|
||||||
if (target.matches('select[name="closet_list[visibility]"]')) {
|
|
||||||
target.closest("form").setAttribute("data-list-visibility", target.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
(function () {
|
|
||||||
$("span.choose-outfit select").change(function (e) {
|
|
||||||
var select = $(this);
|
|
||||||
select.closest("li").find("input[type=text]").val(select.val());
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
function setFormStateCookie(value) {
|
|
||||||
const thirtyDays = 60 * 60 * 24 * 30;
|
|
||||||
document.cookie = `DTIItemPageUserListsFormState=${value};max-age=${thirtyDays}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
|
||||||
if (event.target.matches(".item-header .user-lists-form-opener")) {
|
|
||||||
const header = event.target.closest(".item-header");
|
|
||||||
const form = header.querySelector(".user-lists-form");
|
|
||||||
if (form.hasAttribute("hidden")) {
|
|
||||||
form.removeAttribute("hidden");
|
|
||||||
setFormStateCookie("open");
|
|
||||||
} else {
|
|
||||||
form.setAttribute("hidden", "");
|
|
||||||
setFormStateCookie("closed");
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
// When the species face picker changes, update and submit the main picker form.
|
|
||||||
document.addEventListener("change", (e) => {
|
|
||||||
if (!e.target.matches("species-face-picker")) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mainPickerForm = document.querySelector(
|
|
||||||
"#item-preview species-color-picker form",
|
|
||||||
);
|
|
||||||
const mainSpeciesField = mainPickerForm.querySelector(
|
|
||||||
"[name='preview[species_id]']",
|
|
||||||
);
|
|
||||||
mainSpeciesField.value = e.target.value;
|
|
||||||
mainPickerForm.requestSubmit(); // `submit` doesn't get captured by Turbo!
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Couldn't update species picker: ", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the preview frame fails to load, try a full pageload.
|
|
||||||
document.addEventListener("turbo:frame-missing", (e) => {
|
|
||||||
if (!e.target.matches("#item-preview")) return;
|
|
||||||
|
|
||||||
e.detail.visit(e.detail.response.url);
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
class SpeciesColorPicker extends HTMLElement {
|
|
||||||
#internals;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.#internals = this.attachInternals();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// Listen for changes to auto-submit the form, then tell CSS about it!
|
|
||||||
this.addEventListener("change", this.#handleChange);
|
|
||||||
this.#internals.states.add("auto-loading");
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleChange(e) {
|
|
||||||
this.querySelector("form").requestSubmit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpeciesFacePicker extends HTMLElement {
|
|
||||||
connectedCallback() {
|
|
||||||
this.addEventListener("click", this.#handleClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this.querySelector("input[type=radio]:checked")?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleClick(e) {
|
|
||||||
if (e.target.matches("input[type=radio]")) {
|
|
||||||
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpeciesFacePickerOptions extends HTMLElement {
|
|
||||||
static observedAttributes = ["inert", "aria-hidden"];
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// Once this component is loaded, we stop being inert and aria-hidden. We're ready!
|
|
||||||
this.#activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback() {
|
|
||||||
// If a Turbo Frame tries to morph us into being inert again, activate again!
|
|
||||||
// (It's important that the server's HTML always return `inert`, for progressive
|
|
||||||
// enhancement; and it's important to morph this element, so radio focus state
|
|
||||||
// is preserved. To thread that needle, we have to monitor and remove!)
|
|
||||||
this.#activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
#activate() {
|
|
||||||
this.removeAttribute("inert");
|
|
||||||
this.removeAttribute("aria-hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MeasuredContent extends HTMLElement {
|
|
||||||
connectedCallback() {
|
|
||||||
setTimeout(() => this.#measure(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#measure() {
|
|
||||||
// Find our `<measured-container>` parent, and set our natural width
|
|
||||||
// as `var(--natural-width)` in the context of its CSS styles.
|
|
||||||
const container = this.closest("measured-container");
|
|
||||||
if (container == null) {
|
|
||||||
throw new Error(`<measured-content> must be in a <measured-container>`);
|
|
||||||
}
|
|
||||||
container.style.setProperty("--natural-width", this.offsetWidth + "px");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("species-color-picker", SpeciesColorPicker);
|
|
||||||
customElements.define("species-face-picker", SpeciesFacePicker);
|
|
||||||
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
|
||||||
customElements.define("measured-content", MeasuredContent);
|
|
||||||
15
app/assets/javascripts/lib/easeljs.min.js
vendored
|
|
@ -1,850 +0,0 @@
|
||||||
// https://raw.githubusercontent.com/bigskysoftware/idiomorph/v0.3.0/dist/idiomorph.js
|
|
||||||
|
|
||||||
// base IIFE to define idiomorph
|
|
||||||
var Idiomorph = (function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// AND NOW IT BEGINS...
|
|
||||||
//=============================================================================
|
|
||||||
let EMPTY_SET = new Set();
|
|
||||||
|
|
||||||
// default configuration values, updatable by users now
|
|
||||||
let defaults = {
|
|
||||||
morphStyle: "outerHTML",
|
|
||||||
callbacks : {
|
|
||||||
beforeNodeAdded: noOp,
|
|
||||||
afterNodeAdded: noOp,
|
|
||||||
beforeNodeMorphed: noOp,
|
|
||||||
afterNodeMorphed: noOp,
|
|
||||||
beforeNodeRemoved: noOp,
|
|
||||||
afterNodeRemoved: noOp,
|
|
||||||
beforeAttributeUpdated: noOp,
|
|
||||||
|
|
||||||
},
|
|
||||||
head: {
|
|
||||||
style: 'merge',
|
|
||||||
shouldPreserve: function (elt) {
|
|
||||||
return elt.getAttribute("im-preserve") === "true";
|
|
||||||
},
|
|
||||||
shouldReAppend: function (elt) {
|
|
||||||
return elt.getAttribute("im-re-append") === "true";
|
|
||||||
},
|
|
||||||
shouldRemove: noOp,
|
|
||||||
afterHeadMorphed: noOp,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
|
||||||
//=============================================================================
|
|
||||||
function morph(oldNode, newContent, config = {}) {
|
|
||||||
|
|
||||||
if (oldNode instanceof Document) {
|
|
||||||
oldNode = oldNode.documentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof newContent === 'string') {
|
|
||||||
newContent = parseContent(newContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalizedContent = normalizeContent(newContent);
|
|
||||||
|
|
||||||
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
|
||||||
|
|
||||||
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
|
||||||
if (ctx.head.block) {
|
|
||||||
let oldHead = oldNode.querySelector('head');
|
|
||||||
let newHead = normalizedNewContent.querySelector('head');
|
|
||||||
if (oldHead && newHead) {
|
|
||||||
let promises = handleHeadElement(newHead, oldHead, ctx);
|
|
||||||
// when head promises resolve, call morph again, ignoring the head tag
|
|
||||||
Promise.all(promises).then(function () {
|
|
||||||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
||||||
head: {
|
|
||||||
block: false,
|
|
||||||
ignore: true
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.morphStyle === "innerHTML") {
|
|
||||||
|
|
||||||
// innerHTML, so we are only updating the children
|
|
||||||
morphChildren(normalizedNewContent, oldNode, ctx);
|
|
||||||
return oldNode.children;
|
|
||||||
|
|
||||||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
|
||||||
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
|
||||||
// into either side of the best match
|
|
||||||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
||||||
|
|
||||||
// stash the siblings that will need to be inserted on either side of the best match
|
|
||||||
let previousSibling = bestMatch?.previousSibling;
|
|
||||||
let nextSibling = bestMatch?.nextSibling;
|
|
||||||
|
|
||||||
// morph it
|
|
||||||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
||||||
|
|
||||||
if (bestMatch) {
|
|
||||||
// if there was a best match, merge the siblings in too and return the
|
|
||||||
// whole bunch
|
|
||||||
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
||||||
} else {
|
|
||||||
// otherwise nothing was added to the DOM
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param possibleActiveElement
|
|
||||||
* @param ctx
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
||||||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param oldNode root node to merge content into
|
|
||||||
* @param newContent new content to merge
|
|
||||||
* @param ctx the merge context
|
|
||||||
* @returns {Element} the element that ended up in the DOM
|
|
||||||
*/
|
|
||||||
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
||||||
if (ctx.ignoreActive && oldNode === document.activeElement) {
|
|
||||||
// don't morph focused element
|
|
||||||
} else if (newContent == null) {
|
|
||||||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
||||||
|
|
||||||
oldNode.remove();
|
|
||||||
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
||||||
return null;
|
|
||||||
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
||||||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
||||||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
|
||||||
|
|
||||||
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
||||||
ctx.callbacks.afterNodeAdded(newContent);
|
|
||||||
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
||||||
return newContent;
|
|
||||||
} else {
|
|
||||||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
|
||||||
|
|
||||||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {
|
|
||||||
// ignore the head element
|
|
||||||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
|
||||||
handleHeadElement(newContent, oldNode, ctx);
|
|
||||||
} else {
|
|
||||||
syncNodeFrom(newContent, oldNode, ctx);
|
|
||||||
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
||||||
morphChildren(newContent, oldNode, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
||||||
return oldNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
|
||||||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
|
||||||
* by using id sets, we are able to better match up with content deeper in the DOM.
|
|
||||||
*
|
|
||||||
* Basic algorithm is, for each node in the new content:
|
|
||||||
*
|
|
||||||
* - if we have reached the end of the old parent, append the new content
|
|
||||||
* - if the new content has an id set match with the current insertion point, morph
|
|
||||||
* - search for an id set match
|
|
||||||
* - if id set match found, morph
|
|
||||||
* - otherwise search for a "soft" match
|
|
||||||
* - if a soft match is found, morph
|
|
||||||
* - otherwise, prepend the new node before the current insertion point
|
|
||||||
*
|
|
||||||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
||||||
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
||||||
*
|
|
||||||
* @param {Element} newParent the parent element of the new content
|
|
||||||
* @param {Element } oldParent the old content that we are merging the new content into
|
|
||||||
* @param ctx the merge context
|
|
||||||
*/
|
|
||||||
function morphChildren(newParent, oldParent, ctx) {
|
|
||||||
|
|
||||||
let nextNewChild = newParent.firstChild;
|
|
||||||
let insertionPoint = oldParent.firstChild;
|
|
||||||
let newChild;
|
|
||||||
|
|
||||||
// run through all the new content
|
|
||||||
while (nextNewChild) {
|
|
||||||
|
|
||||||
newChild = nextNewChild;
|
|
||||||
nextNewChild = newChild.nextSibling;
|
|
||||||
|
|
||||||
// if we are at the end of the exiting parent's children, just append
|
|
||||||
if (insertionPoint == null) {
|
|
||||||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
||||||
|
|
||||||
oldParent.appendChild(newChild);
|
|
||||||
ctx.callbacks.afterNodeAdded(newChild);
|
|
||||||
removeIdsFromConsideration(ctx, newChild);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the current node has an id set match then morph
|
|
||||||
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
|
||||||
morphOldNodeTo(insertionPoint, newChild, ctx);
|
|
||||||
insertionPoint = insertionPoint.nextSibling;
|
|
||||||
removeIdsFromConsideration(ctx, newChild);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise search forward in the existing old children for an id set match
|
|
||||||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
||||||
|
|
||||||
// if we found a potential match, remove the nodes until that point and morph
|
|
||||||
if (idSetMatch) {
|
|
||||||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
|
||||||
morphOldNodeTo(idSetMatch, newChild, ctx);
|
|
||||||
removeIdsFromConsideration(ctx, newChild);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no id set match found, so scan forward for a soft match for the current node
|
|
||||||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
||||||
|
|
||||||
// if we found a soft match for the current node, morph
|
|
||||||
if (softMatch) {
|
|
||||||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
|
||||||
morphOldNodeTo(softMatch, newChild, ctx);
|
|
||||||
removeIdsFromConsideration(ctx, newChild);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// abandon all hope of morphing, just insert the new child before the insertion point
|
|
||||||
// and move on
|
|
||||||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
||||||
|
|
||||||
oldParent.insertBefore(newChild, insertionPoint);
|
|
||||||
ctx.callbacks.afterNodeAdded(newChild);
|
|
||||||
removeIdsFromConsideration(ctx, newChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove any remaining old nodes that didn't match up with new content
|
|
||||||
while (insertionPoint !== null) {
|
|
||||||
|
|
||||||
let tempNode = insertionPoint;
|
|
||||||
insertionPoint = insertionPoint.nextSibling;
|
|
||||||
removeNode(tempNode, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Attribute Syncing Code
|
|
||||||
//=============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param attr {String} the attribute to be mutated
|
|
||||||
* @param to {Element} the element that is going to be updated
|
|
||||||
* @param updateType {("update"|"remove")}
|
|
||||||
* @param ctx the merge context
|
|
||||||
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
||||||
*/
|
|
||||||
function ignoreAttribute(attr, to, updateType, ctx) {
|
|
||||||
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* syncs a given node with another node, copying over all attributes and
|
|
||||||
* inner element state from the 'from' node to the 'to' node
|
|
||||||
*
|
|
||||||
* @param {Element} from the element to copy attributes & state from
|
|
||||||
* @param {Element} to the element to copy attributes & state to
|
|
||||||
* @param ctx the merge context
|
|
||||||
*/
|
|
||||||
function syncNodeFrom(from, to, ctx) {
|
|
||||||
let type = from.nodeType
|
|
||||||
|
|
||||||
// if is an element type, sync the attributes from the
|
|
||||||
// new node into the new node
|
|
||||||
if (type === 1 /* element type */) {
|
|
||||||
const fromAttributes = from.attributes;
|
|
||||||
const toAttributes = to.attributes;
|
|
||||||
for (const fromAttribute of fromAttributes) {
|
|
||||||
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
||||||
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// iterate backwards to avoid skipping over items when a delete occurs
|
|
||||||
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
|
||||||
const toAttribute = toAttributes[i];
|
|
||||||
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!from.hasAttribute(toAttribute.name)) {
|
|
||||||
to.removeAttribute(toAttribute.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sync text nodes
|
|
||||||
if (type === 8 /* comment */ || type === 3 /* text */) {
|
|
||||||
if (to.nodeValue !== from.nodeValue) {
|
|
||||||
to.nodeValue = from.nodeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreValueOfActiveElement(to, ctx)) {
|
|
||||||
// sync input values
|
|
||||||
syncInputValue(from, to, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param from {Element} element to sync the value from
|
|
||||||
* @param to {Element} element to sync the value to
|
|
||||||
* @param attributeName {String} the attribute name
|
|
||||||
* @param ctx the merge context
|
|
||||||
*/
|
|
||||||
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
|
||||||
if (from[attributeName] !== to[attributeName]) {
|
|
||||||
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
|
||||||
if (!ignoreUpdate) {
|
|
||||||
to[attributeName] = from[attributeName];
|
|
||||||
}
|
|
||||||
if (from[attributeName]) {
|
|
||||||
if (!ignoreUpdate) {
|
|
||||||
to.setAttribute(attributeName, from[attributeName]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
|
||||||
to.removeAttribute(attributeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NB: many bothans died to bring us information:
|
|
||||||
*
|
|
||||||
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
|
||||||
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
|
||||||
*
|
|
||||||
* @param from {Element} the element to sync the input value from
|
|
||||||
* @param to {Element} the element to sync the input value to
|
|
||||||
* @param ctx the merge context
|
|
||||||
*/
|
|
||||||
function syncInputValue(from, to, ctx) {
|
|
||||||
if (from instanceof HTMLInputElement &&
|
|
||||||
to instanceof HTMLInputElement &&
|
|
||||||
from.type !== 'file') {
|
|
||||||
|
|
||||||
let fromValue = from.value;
|
|
||||||
let toValue = to.value;
|
|
||||||
|
|
||||||
// sync boolean attributes
|
|
||||||
syncBooleanAttribute(from, to, 'checked', ctx);
|
|
||||||
syncBooleanAttribute(from, to, 'disabled', ctx);
|
|
||||||
|
|
||||||
if (!from.hasAttribute('value')) {
|
|
||||||
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
|
||||||
to.value = '';
|
|
||||||
to.removeAttribute('value');
|
|
||||||
}
|
|
||||||
} else if (fromValue !== toValue) {
|
|
||||||
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
|
||||||
to.setAttribute('value', fromValue);
|
|
||||||
to.value = fromValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (from instanceof HTMLOptionElement) {
|
|
||||||
syncBooleanAttribute(from, to, 'selected', ctx)
|
|
||||||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
||||||
let fromValue = from.value;
|
|
||||||
let toValue = to.value;
|
|
||||||
if (ignoreAttribute('value', to, 'update', ctx)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fromValue !== toValue) {
|
|
||||||
to.value = fromValue;
|
|
||||||
}
|
|
||||||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
||||||
to.firstChild.nodeValue = fromValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
||||||
//=============================================================================
|
|
||||||
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
|
||||||
|
|
||||||
let added = []
|
|
||||||
let removed = []
|
|
||||||
let preserved = []
|
|
||||||
let nodesToAppend = []
|
|
||||||
|
|
||||||
let headMergeStyle = ctx.head.style;
|
|
||||||
|
|
||||||
// put all new head elements into a Map, by their outerHTML
|
|
||||||
let srcToNewHeadNodes = new Map();
|
|
||||||
for (const newHeadChild of newHeadTag.children) {
|
|
||||||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each elt in the current head
|
|
||||||
for (const currentHeadElt of currentHead.children) {
|
|
||||||
|
|
||||||
// If the current head element is in the map
|
|
||||||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
||||||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
|
||||||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
|
||||||
if (inNewContent || isPreserved) {
|
|
||||||
if (isReAppended) {
|
|
||||||
// remove the current version and let the new version replace it and re-execute
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
} else {
|
|
||||||
// this element already exists and should not be re-appended, so remove it from
|
|
||||||
// the new content map, preserving it in the DOM
|
|
||||||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
||||||
preserved.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (headMergeStyle === "append") {
|
|
||||||
// we are appending and this existing element is not new content
|
|
||||||
// so if and only if it is marked for re-append do we do anything
|
|
||||||
if (isReAppended) {
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
nodesToAppend.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if this is a merge, we remove this content since it is not in the new head
|
|
||||||
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the remaining new head elements in the Map into the
|
|
||||||
// nodes to append to the head tag
|
|
||||||
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
||||||
log("to append: ", nodesToAppend);
|
|
||||||
|
|
||||||
let promises = [];
|
|
||||||
for (const newNode of nodesToAppend) {
|
|
||||||
log("adding: ", newNode);
|
|
||||||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
|
||||||
log(newElt);
|
|
||||||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
||||||
if (newElt.href || newElt.src) {
|
|
||||||
let resolve = null;
|
|
||||||
let promise = new Promise(function (_resolve) {
|
|
||||||
resolve = _resolve;
|
|
||||||
});
|
|
||||||
newElt.addEventListener('load', function () {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
promises.push(promise);
|
|
||||||
}
|
|
||||||
currentHead.appendChild(newElt);
|
|
||||||
ctx.callbacks.afterNodeAdded(newElt);
|
|
||||||
added.push(newElt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all removed elements, after we have appended the new elements to avoid
|
|
||||||
// additional network requests for things like style sheets
|
|
||||||
for (const removedElement of removed) {
|
|
||||||
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
||||||
currentHead.removeChild(removedElement);
|
|
||||||
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
|
||||||
return promises;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Misc
|
|
||||||
//=============================================================================
|
|
||||||
|
|
||||||
function log() {
|
|
||||||
//console.log(arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
function noOp() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Deep merges the config object and the Idiomoroph.defaults object to
|
|
||||||
produce a final configuration object
|
|
||||||
*/
|
|
||||||
function mergeDefaults(config) {
|
|
||||||
let finalConfig = {};
|
|
||||||
// copy top level stuff into final config
|
|
||||||
Object.assign(finalConfig, defaults);
|
|
||||||
Object.assign(finalConfig, config);
|
|
||||||
|
|
||||||
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
||||||
finalConfig.callbacks = {};
|
|
||||||
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
|
||||||
Object.assign(finalConfig.callbacks, config.callbacks);
|
|
||||||
|
|
||||||
// copy head config into final config (do this to deep merge the head)
|
|
||||||
finalConfig.head = {};
|
|
||||||
Object.assign(finalConfig.head, defaults.head);
|
|
||||||
Object.assign(finalConfig.head, config.head);
|
|
||||||
return finalConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMorphContext(oldNode, newContent, config) {
|
|
||||||
config = mergeDefaults(config);
|
|
||||||
return {
|
|
||||||
target: oldNode,
|
|
||||||
newContent: newContent,
|
|
||||||
config: config,
|
|
||||||
morphStyle: config.morphStyle,
|
|
||||||
ignoreActive: config.ignoreActive,
|
|
||||||
ignoreActiveValue: config.ignoreActiveValue,
|
|
||||||
idMap: createIdMap(oldNode, newContent),
|
|
||||||
deadIds: new Set(),
|
|
||||||
callbacks: config.callbacks,
|
|
||||||
head: config.head
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIdSetMatch(node1, node2, ctx) {
|
|
||||||
if (node1 == null || node2 == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
|
||||||
if (node1.id !== "" && node1.id === node2.id) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSoftMatch(node1, node2) {
|
|
||||||
if (node1 == null || node2 == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
||||||
while (startInclusive !== endExclusive) {
|
|
||||||
let tempNode = startInclusive;
|
|
||||||
startInclusive = startInclusive.nextSibling;
|
|
||||||
removeNode(tempNode, ctx);
|
|
||||||
}
|
|
||||||
removeIdsFromConsideration(ctx, endExclusive);
|
|
||||||
return endExclusive.nextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
||||||
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
||||||
// if the number of potential id matches we are discarding is greater than the
|
|
||||||
// potential id matches for the new child
|
|
||||||
//=============================================================================
|
|
||||||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
||||||
|
|
||||||
// max id matches we are willing to discard in our search
|
|
||||||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
|
||||||
|
|
||||||
let potentialMatch = null;
|
|
||||||
|
|
||||||
// only search forward if there is a possibility of an id match
|
|
||||||
if (newChildPotentialIdCount > 0) {
|
|
||||||
let potentialMatch = insertionPoint;
|
|
||||||
// if there is a possibility of an id match, scan forward
|
|
||||||
// keep track of the potential id match count we are discarding (the
|
|
||||||
// newChildPotentialIdCount must be greater than this to make it likely
|
|
||||||
// worth it)
|
|
||||||
let otherMatchCount = 0;
|
|
||||||
while (potentialMatch != null) {
|
|
||||||
|
|
||||||
// If we have an id match, return the current potential match
|
|
||||||
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
||||||
return potentialMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// computer the other potential matches of this new content
|
|
||||||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
||||||
if (otherMatchCount > newChildPotentialIdCount) {
|
|
||||||
// if we have more potential id matches in _other_ content, we
|
|
||||||
// do not have a good candidate for an id match, so return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// advanced to the next old content child
|
|
||||||
potentialMatch = potentialMatch.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return potentialMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
|
||||||
// for the newChild. We stop if we find a potential soft match for the new child OR
|
|
||||||
// if we find a potential id match in the old parents children OR if we find two
|
|
||||||
// potential soft matches for the next two pieces of new content
|
|
||||||
//=============================================================================
|
|
||||||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
||||||
|
|
||||||
let potentialSoftMatch = insertionPoint;
|
|
||||||
let nextSibling = newChild.nextSibling;
|
|
||||||
let siblingSoftMatchCount = 0;
|
|
||||||
|
|
||||||
while (potentialSoftMatch != null) {
|
|
||||||
|
|
||||||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
|
||||||
// the current potential soft match has a potential id set match with the remaining new
|
|
||||||
// content so bail out of looking
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a soft match with the current node, return it
|
|
||||||
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
||||||
return potentialSoftMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
|
||||||
// the next new node has a soft match with this node, so
|
|
||||||
// increment the count of future soft matches
|
|
||||||
siblingSoftMatchCount++;
|
|
||||||
nextSibling = nextSibling.nextSibling;
|
|
||||||
|
|
||||||
// If there are two future soft matches, bail to allow the siblings to soft match
|
|
||||||
// so that we don't consume future soft matches for the sake of the current node
|
|
||||||
if (siblingSoftMatchCount >= 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// advanced to the next old content child
|
|
||||||
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
return potentialSoftMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseContent(newContent) {
|
|
||||||
let parser = new DOMParser();
|
|
||||||
|
|
||||||
// remove svgs to avoid false-positive matches on head, etc.
|
|
||||||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
||||||
|
|
||||||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
||||||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
||||||
let content = parser.parseFromString(newContent, "text/html");
|
|
||||||
// if it is a full HTML document, return the document itself as the parent container
|
|
||||||
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
||||||
content.generatedByIdiomorph = true;
|
|
||||||
return content;
|
|
||||||
} else {
|
|
||||||
// otherwise return the html element as the parent container
|
|
||||||
let htmlElement = content.firstChild;
|
|
||||||
if (htmlElement) {
|
|
||||||
htmlElement.generatedByIdiomorph = true;
|
|
||||||
return htmlElement;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
||||||
// deal with touchy tags like tr, tbody, etc.
|
|
||||||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
||||||
let content = responseDoc.body.querySelector('template').content;
|
|
||||||
content.generatedByIdiomorph = true;
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeContent(newContent) {
|
|
||||||
if (newContent == null) {
|
|
||||||
// noinspection UnnecessaryLocalVariableJS
|
|
||||||
const dummyParent = document.createElement('div');
|
|
||||||
return dummyParent;
|
|
||||||
} else if (newContent.generatedByIdiomorph) {
|
|
||||||
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
||||||
return newContent;
|
|
||||||
} else if (newContent instanceof Node) {
|
|
||||||
// a single node is added as a child to a dummy parent
|
|
||||||
const dummyParent = document.createElement('div');
|
|
||||||
dummyParent.append(newContent);
|
|
||||||
return dummyParent;
|
|
||||||
} else {
|
|
||||||
// all nodes in the array or HTMLElement collection are consolidated under
|
|
||||||
// a single dummy parent element
|
|
||||||
const dummyParent = document.createElement('div');
|
|
||||||
for (const elt of [...newContent]) {
|
|
||||||
dummyParent.append(elt);
|
|
||||||
}
|
|
||||||
return dummyParent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
||||||
let stack = []
|
|
||||||
let added = []
|
|
||||||
while (previousSibling != null) {
|
|
||||||
stack.push(previousSibling);
|
|
||||||
previousSibling = previousSibling.previousSibling;
|
|
||||||
}
|
|
||||||
while (stack.length > 0) {
|
|
||||||
let node = stack.pop();
|
|
||||||
added.push(node); // push added preceding siblings on in order and insert
|
|
||||||
morphedNode.parentElement.insertBefore(node, morphedNode);
|
|
||||||
}
|
|
||||||
added.push(morphedNode);
|
|
||||||
while (nextSibling != null) {
|
|
||||||
stack.push(nextSibling);
|
|
||||||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
|
||||||
nextSibling = nextSibling.nextSibling;
|
|
||||||
}
|
|
||||||
while (stack.length > 0) {
|
|
||||||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
|
||||||
}
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
||||||
let currentElement;
|
|
||||||
currentElement = newContent.firstChild;
|
|
||||||
let bestElement = currentElement;
|
|
||||||
let score = 0;
|
|
||||||
while (currentElement) {
|
|
||||||
let newScore = scoreElement(currentElement, oldNode, ctx);
|
|
||||||
if (newScore > score) {
|
|
||||||
bestElement = currentElement;
|
|
||||||
score = newScore;
|
|
||||||
}
|
|
||||||
currentElement = currentElement.nextSibling;
|
|
||||||
}
|
|
||||||
return bestElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreElement(node1, node2, ctx) {
|
|
||||||
if (isSoftMatch(node1, node2)) {
|
|
||||||
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNode(tempNode, ctx) {
|
|
||||||
removeIdsFromConsideration(ctx, tempNode)
|
|
||||||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
|
||||||
|
|
||||||
tempNode.remove();
|
|
||||||
ctx.callbacks.afterNodeRemoved(tempNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// ID Set Functions
|
|
||||||
//=============================================================================
|
|
||||||
|
|
||||||
function isIdInConsideration(ctx, id) {
|
|
||||||
return !ctx.deadIds.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function idIsWithinNode(ctx, id, targetNode) {
|
|
||||||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
|
||||||
return idSet.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeIdsFromConsideration(ctx, node) {
|
|
||||||
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
|
||||||
for (const id of idSet) {
|
|
||||||
ctx.deadIds.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIdIntersectionCount(ctx, node1, node2) {
|
|
||||||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
|
||||||
let matchCount = 0;
|
|
||||||
for (const id of sourceSet) {
|
|
||||||
// a potential match is an id in the source and potentialIdsSet, but
|
|
||||||
// that has not already been merged into the DOM
|
|
||||||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
|
||||||
++matchCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bottom up algorithm that finds all elements with ids inside of the node
|
|
||||||
* argument and populates id sets for those nodes and all their parents, generating
|
|
||||||
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
|
||||||
*
|
|
||||||
* @param node {Element}
|
|
||||||
* @param {Map<Node, Set<String>>} idMap
|
|
||||||
*/
|
|
||||||
function populateIdMapForNode(node, idMap) {
|
|
||||||
let nodeParent = node.parentElement;
|
|
||||||
// find all elements with an id property
|
|
||||||
let idElements = node.querySelectorAll('[id]');
|
|
||||||
for (const elt of idElements) {
|
|
||||||
let current = elt;
|
|
||||||
// walk up the parent hierarchy of that element, adding the id
|
|
||||||
// of element to the parent's id set
|
|
||||||
while (current !== nodeParent && current != null) {
|
|
||||||
let idSet = idMap.get(current);
|
|
||||||
// if the id set doesn't exist, create it and insert it in the map
|
|
||||||
if (idSet == null) {
|
|
||||||
idSet = new Set();
|
|
||||||
idMap.set(current, idSet);
|
|
||||||
}
|
|
||||||
idSet.add(elt.id);
|
|
||||||
current = current.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
|
||||||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
|
||||||
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
|
||||||
* to contribute to a parent nodes matching.
|
|
||||||
*
|
|
||||||
* @param {Element} oldContent the old content that will be morphed
|
|
||||||
* @param {Element} newContent the new content to morph to
|
|
||||||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
|
||||||
*/
|
|
||||||
function createIdMap(oldContent, newContent) {
|
|
||||||
let idMap = new Map();
|
|
||||||
populateIdMapForNode(oldContent, idMap);
|
|
||||||
populateIdMapForNode(newContent, idMap);
|
|
||||||
return idMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// This is what ends up becoming the Idiomorph global object
|
|
||||||
//=============================================================================
|
|
||||||
return {
|
|
||||||
morph,
|
|
||||||
defaults
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
(function($){
|
|
||||||
$.jGrowl=function(m,o){
|
|
||||||
if($("#jGrowl").size()==0){
|
|
||||||
$("<div id=\"jGrowl\"></div>").addClass($.jGrowl.defaults.position).appendTo("body");
|
|
||||||
}
|
|
||||||
$("#jGrowl").jGrowl(m,o);
|
|
||||||
};
|
|
||||||
$.fn.jGrowl=function(m,o){
|
|
||||||
if($.isFunction(this.each)){
|
|
||||||
var _6=arguments;
|
|
||||||
return this.each(function(){
|
|
||||||
var _7=this;
|
|
||||||
if($(this).data("jGrowl.instance")==undefined){
|
|
||||||
$(this).data("jGrowl.instance",$.extend(new $.fn.jGrowl(),{notifications:[],element:null,interval:null}));
|
|
||||||
$(this).data("jGrowl.instance").startup(this);
|
|
||||||
}
|
|
||||||
if($.isFunction($(this).data("jGrowl.instance")[m])){
|
|
||||||
$(this).data("jGrowl.instance")[m].apply($(this).data("jGrowl.instance"),$.makeArray(_6).slice(1));
|
|
||||||
}else{
|
|
||||||
$(this).data("jGrowl.instance").create(m,o);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$.extend($.fn.jGrowl.prototype,{defaults:{pool:0,header:"",group:"",sticky:false,position:"top-right",glue:"after",theme:"default",corners:"10px",check:250,life:3000,speed:"normal",easing:"swing",closer:true,closeTemplate:"×",closerTemplate:"<div>[ close all ]</div>",log:function(e,m,o){
|
|
||||||
},beforeOpen:function(e,m,o){
|
|
||||||
},open:function(e,m,o){
|
|
||||||
},beforeClose:function(e,m,o){
|
|
||||||
},close:function(e,m,o){
|
|
||||||
},animateOpen:{opacity:"show"},animateClose:{opacity:"hide"}},notifications:[],element:null,interval:null,create:function(_17,o){
|
|
||||||
var o=$.extend({},this.defaults,o);
|
|
||||||
this.notifications.push({message:_17,options:o});
|
|
||||||
o.log.apply(this.element,[this.element,_17,o]);
|
|
||||||
},render:function(_19){
|
|
||||||
var _1a=this;
|
|
||||||
var _1b=_19.message;
|
|
||||||
var o=_19.options;
|
|
||||||
var _19=$("<div class=\"jGrowl-notification ui-state-highlight ui-corner-all"+((o.group!=undefined&&o.group!="")?" "+o.group:"")+"\">"+"<div class=\"close\">"+o.closeTemplate+"</div>"+"<div class=\"header\">"+o.header+"</div>"+"<div class=\"message\">"+_1b+"</div></div>").data("jGrowl",o).addClass(o.theme).children("div.close").bind("click.jGrowl",function(){
|
|
||||||
$(this).parent().trigger("jGrowl.close");
|
|
||||||
}).parent();
|
|
||||||
$(_19).bind("mouseover.jGrowl",function(){
|
|
||||||
$("div.jGrowl-notification",_1a.element).data("jGrowl.pause",true);
|
|
||||||
}).bind("mouseout.jGrowl",function(){
|
|
||||||
$("div.jGrowl-notification",_1a.element).data("jGrowl.pause",false);
|
|
||||||
}).bind("jGrowl.beforeOpen",function(){
|
|
||||||
if(o.beforeOpen.apply(_19,[_19,_1b,o,_1a.element])!=false){
|
|
||||||
$(this).trigger("jGrowl.open");
|
|
||||||
}
|
|
||||||
}).bind("jGrowl.open",function(){
|
|
||||||
if(o.open.apply(_19,[_19,_1b,o,_1a.element])!=false){
|
|
||||||
if(o.glue=="after"){
|
|
||||||
$("div.jGrowl-notification:last",_1a.element).after(_19);
|
|
||||||
}else{
|
|
||||||
$("div.jGrowl-notification:first",_1a.element).before(_19);
|
|
||||||
}
|
|
||||||
$(this).animate(o.animateOpen,o.speed,o.easing,function(){
|
|
||||||
if($.browser.msie&&(parseInt($(this).css("opacity"),10)===1||parseInt($(this).css("opacity"),10)===0)){
|
|
||||||
this.style.removeAttribute("filter");
|
|
||||||
}
|
|
||||||
$(this).data("jGrowl").created=new Date();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).bind("jGrowl.beforeClose",function(){
|
|
||||||
if(o.beforeClose.apply(_19,[_19,_1b,o,_1a.element])!=false){
|
|
||||||
$(this).trigger("jGrowl.close");
|
|
||||||
}
|
|
||||||
}).bind("jGrowl.close",function(){
|
|
||||||
$(this).data("jGrowl.pause",true);
|
|
||||||
$(this).animate(o.animateClose,o.speed,o.easing,function(){
|
|
||||||
$(this).remove();
|
|
||||||
var _1d=o.close.apply(_19,[_19,_1b,o,_1a.element]);
|
|
||||||
if($.isFunction(_1d)){
|
|
||||||
_1d.apply(_19,[_19,_1b,o,_1a.element]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).trigger("jGrowl.beforeOpen");
|
|
||||||
if($.fn.corner!=undefined){
|
|
||||||
$(_19).corner(o.corners);
|
|
||||||
}
|
|
||||||
if($("div.jGrowl-notification:parent",_1a.element).size()>1&&$("div.jGrowl-closer",_1a.element).size()==0&&this.defaults.closer!=false){
|
|
||||||
$(this.defaults.closerTemplate).addClass("jGrowl-closer ui-state-highlight ui-corner-all").addClass(this.defaults.theme).appendTo(_1a.element).animate(this.defaults.animateOpen,this.defaults.speed,this.defaults.easing).bind("click.jGrowl",function(){
|
|
||||||
$(this).siblings().children("div.close").trigger("click.jGrowl");
|
|
||||||
if($.isFunction(_1a.defaults.closer)){
|
|
||||||
_1a.defaults.closer.apply($(this).parent()[0],[$(this).parent()[0]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},update:function(){
|
|
||||||
$(this.element).find("div.jGrowl-notification:parent").each(function(){
|
|
||||||
if($(this).data("jGrowl")!=undefined&&$(this).data("jGrowl").created!=undefined&&($(this).data("jGrowl").created.getTime()+$(this).data("jGrowl").life)<(new Date()).getTime()&&$(this).data("jGrowl").sticky!=true&&($(this).data("jGrowl.pause")==undefined||$(this).data("jGrowl.pause")!=true)){
|
|
||||||
$(this).trigger("jGrowl.beforeClose");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(this.notifications.length>0&&(this.defaults.pool==0||$(this.element).find("div.jGrowl-notification:parent").size()<this.defaults.pool)){
|
|
||||||
this.render(this.notifications.shift());
|
|
||||||
}
|
|
||||||
if($(this.element).find("div.jGrowl-notification:parent").size()<2){
|
|
||||||
$(this.element).find("div.jGrowl-closer").animate(this.defaults.animateClose,this.defaults.speed,this.defaults.easing,function(){
|
|
||||||
$(this).remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},startup:function(e){
|
|
||||||
this.element=$(e).addClass("jGrowl").append("<div class=\"jGrowl-notification\"></div>");
|
|
||||||
this.interval=setInterval(function(){
|
|
||||||
$(e).data("jGrowl.instance").update();
|
|
||||||
},this.defaults.check);
|
|
||||||
if($.browser.msie&&parseInt($.browser.version)<7&&!window["XMLHttpRequest"]){
|
|
||||||
$(this.element).addClass("ie6");
|
|
||||||
}
|
|
||||||
},shutdown:function(){
|
|
||||||
$(this.element).removeClass("jGrowl").find("div.jGrowl-notification").remove();
|
|
||||||
clearInterval(this.interval);
|
|
||||||
},close:function(){
|
|
||||||
$(this.element).find("div.jGrowl-notification").each(function(){
|
|
||||||
$(this).trigger("jGrowl.beforeClose");
|
|
||||||
});
|
|
||||||
}});
|
|
||||||
$.jGrowl.defaults=$.fn.jGrowl.prototype.defaults;
|
|
||||||
})(jQuery);
|
|
||||||
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
/**
|
|
||||||
* Timeago is a jQuery plugin that makes it easy to support automatically
|
|
||||||
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
|
||||||
*
|
|
||||||
* @name timeago
|
|
||||||
* @version 0.11.4
|
|
||||||
* @requires jQuery v1.2.3+
|
|
||||||
* @author Ryan McGeary
|
|
||||||
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
|
||||||
*
|
|
||||||
* For usage and examples, visit:
|
|
||||||
* http://timeago.yarp.com/
|
|
||||||
*
|
|
||||||
* Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
|
||||||
*/
|
|
||||||
(function($) {
|
|
||||||
$.timeago = function(timestamp) {
|
|
||||||
if (timestamp instanceof Date) {
|
|
||||||
return inWords(timestamp);
|
|
||||||
} else if (typeof timestamp === "string") {
|
|
||||||
return inWords($.timeago.parse(timestamp));
|
|
||||||
} else if (typeof timestamp === "number") {
|
|
||||||
return inWords(new Date(timestamp));
|
|
||||||
} else {
|
|
||||||
return inWords($.timeago.datetime(timestamp));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var $t = $.timeago;
|
|
||||||
|
|
||||||
$.extend($.timeago, {
|
|
||||||
settings: {
|
|
||||||
refreshMillis: 60000,
|
|
||||||
allowFuture: false,
|
|
||||||
strings: {
|
|
||||||
prefixAgo: null,
|
|
||||||
prefixFromNow: null,
|
|
||||||
suffixAgo: "ago",
|
|
||||||
suffixFromNow: "from now",
|
|
||||||
seconds: "less than a minute",
|
|
||||||
minute: "about a minute",
|
|
||||||
minutes: "%d minutes",
|
|
||||||
hour: "about an hour",
|
|
||||||
hours: "about %d hours",
|
|
||||||
day: "a day",
|
|
||||||
days: "%d days",
|
|
||||||
month: "about a month",
|
|
||||||
months: "%d months",
|
|
||||||
year: "about a year",
|
|
||||||
years: "%d years",
|
|
||||||
wordSeparator: " ",
|
|
||||||
numbers: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inWords: function(distanceMillis) {
|
|
||||||
var $l = this.settings.strings;
|
|
||||||
var prefix = $l.prefixAgo;
|
|
||||||
var suffix = $l.suffixAgo;
|
|
||||||
if (this.settings.allowFuture) {
|
|
||||||
if (distanceMillis < 0) {
|
|
||||||
prefix = $l.prefixFromNow;
|
|
||||||
suffix = $l.suffixFromNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var seconds = Math.abs(distanceMillis) / 1000;
|
|
||||||
var minutes = seconds / 60;
|
|
||||||
var hours = minutes / 60;
|
|
||||||
var days = hours / 24;
|
|
||||||
var years = days / 365;
|
|
||||||
|
|
||||||
function substitute(stringOrFunction, number) {
|
|
||||||
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
|
||||||
var value = ($l.numbers && $l.numbers[number]) || number;
|
|
||||||
return string.replace(/%d/i, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
|
||||||
seconds < 90 && substitute($l.minute, 1) ||
|
|
||||||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
|
||||||
minutes < 90 && substitute($l.hour, 1) ||
|
|
||||||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
|
||||||
hours < 42 && substitute($l.day, 1) ||
|
|
||||||
days < 30 && substitute($l.days, Math.round(days)) ||
|
|
||||||
days < 45 && substitute($l.month, 1) ||
|
|
||||||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
|
||||||
years < 1.5 && substitute($l.year, 1) ||
|
|
||||||
substitute($l.years, Math.round(years));
|
|
||||||
|
|
||||||
var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator;
|
|
||||||
return $.trim([prefix, words, suffix].join(separator));
|
|
||||||
},
|
|
||||||
parse: function(iso8601) {
|
|
||||||
var s = $.trim(iso8601);
|
|
||||||
s = s.replace(/\.\d+/,""); // remove milliseconds
|
|
||||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
|
||||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
|
||||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
|
||||||
return new Date(s);
|
|
||||||
},
|
|
||||||
datetime: function(elem) {
|
|
||||||
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
|
||||||
return $t.parse(iso8601);
|
|
||||||
},
|
|
||||||
isTime: function(elem) {
|
|
||||||
// jQuery's `is()` doesn't play well with HTML5 in IE
|
|
||||||
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$.fn.timeago = function() {
|
|
||||||
var self = this;
|
|
||||||
self.each(refresh);
|
|
||||||
|
|
||||||
var $s = $t.settings;
|
|
||||||
if ($s.refreshMillis > 0) {
|
|
||||||
setInterval(function() { self.each(refresh); }, $s.refreshMillis);
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
var data = prepareData(this);
|
|
||||||
if (!isNaN(data.datetime)) {
|
|
||||||
$(this).text(inWords(data.datetime));
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareData(element) {
|
|
||||||
element = $(element);
|
|
||||||
if (!element.data("timeago")) {
|
|
||||||
element.data("timeago", { datetime: $t.datetime(element) });
|
|
||||||
var text = $.trim(element.text());
|
|
||||||
if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
|
||||||
element.attr("title", text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return element.data("timeago");
|
|
||||||
}
|
|
||||||
|
|
||||||
function inWords(date) {
|
|
||||||
return $t.inWords(distance(date));
|
|
||||||
}
|
|
||||||
|
|
||||||
function distance(date) {
|
|
||||||
return (new Date().getTime() - date.getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix for IE6 suckage
|
|
||||||
document.createElement("abbr");
|
|
||||||
document.createElement("time");
|
|
||||||
}(jQuery));
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
/*!
|
|
||||||
* jQuery UI 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI
|
|
||||||
*/
|
|
||||||
(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.14",
|
|
||||||
keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,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,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();
|
|
||||||
b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,
|
|
||||||
"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",
|
|
||||||
function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,
|
|
||||||
outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a,"tabindex"),d=isNaN(b);
|
|
||||||
return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=
|
|
||||||
0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&c.ui.isOverAxis(b,e,i)}})}})(jQuery);
|
|
||||||
;/*!
|
|
||||||
* jQuery UI Widget 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Widget
|
|
||||||
*/
|
|
||||||
(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h,
|
|
||||||
a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.charAt(0)==="_")return h;
|
|
||||||
e?this.each(function(){var g=b.data(this,a),i=g&&b.isFunction(g[d])?g[d].apply(g,f):g;if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,
|
|
||||||
this._getCreateOptions(),a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){return b.metadata&&b.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},
|
|
||||||
widget:function(){return this.element},option:function(a,c){var d=a;if(arguments.length===0)return b.extend({},this.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}this._setOptions(d);return this},_setOptions:function(a){var c=this;b.each(a,function(d,e){c._setOption(d,e)});return this},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},
|
|
||||||
enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
|
|
||||||
;/*!
|
|
||||||
* jQuery UI Mouse 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Mouse
|
|
||||||
*
|
|
||||||
* Depends:
|
|
||||||
* jquery.ui.widget.js
|
|
||||||
*/
|
|
||||||
(function(b){var d=false;b(document).mousedown(function(){d=false});b.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(c){return a._mouseDown(c)}).bind("click."+this.widgetName,function(c){if(true===b.data(c.target,a.widgetName+".preventClickEvent")){b.removeData(c.target,a.widgetName+".preventClickEvent");c.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+
|
|
||||||
this.widgetName)},_mouseDown:function(a){if(!d){this._mouseStarted&&this._mouseUp(a);this._mouseDownEvent=a;var c=this,f=a.which==1,g=typeof this.options.cancel=="string"?b(a.target).closest(this.options.cancel).length:false;if(!f||g||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){c.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==
|
|
||||||
false;if(!this._mouseStarted){a.preventDefault();return true}}true===b.data(a.target,this.widgetName+".preventClickEvent")&&b.removeData(a.target,this.widgetName+".preventClickEvent");this._mouseMoveDelegate=function(e){return c._mouseMove(e)};this._mouseUpDelegate=function(e){return c._mouseUp(e)};b(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);a.preventDefault();return d=true}},_mouseMove:function(a){if(b.browser.msie&&
|
|
||||||
!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=
|
|
||||||
false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
|
|
||||||
;/*
|
|
||||||
* jQuery UI Position 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Position
|
|
||||||
*/
|
|
||||||
(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.setTimeout){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j={top:b.of.pageY,
|
|
||||||
left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/2;if(b.at[1]==="bottom")j.top+=
|
|
||||||
k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+(parseInt(c.curCSS(this,"marginRight",true))||0),w=m+q+(parseInt(c.curCSS(this,"marginBottom",true))||0),i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]==="center")i.top-=
|
|
||||||
m/2;i.left=Math.round(i.left);i.top=Math.round(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();b.left=
|
|
||||||
d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];b.left+=
|
|
||||||
a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=c(b),
|
|
||||||
g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
|
|
||||||
;/*
|
|
||||||
* jQuery UI Draggable 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Draggables
|
|
||||||
*
|
|
||||||
* Depends:
|
|
||||||
* jquery.ui.core.js
|
|
||||||
* jquery.ui.mouse.js
|
|
||||||
* jquery.ui.widget.js
|
|
||||||
*/
|
|
||||||
(function(d){d.widget("ui.draggable",d.ui.mouse,{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},_create:function(){if(this.options.helper==
|
|
||||||
"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b=
|
|
||||||
this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options;this.helper=
|
|
||||||
this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});
|
|
||||||
this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true},
|
|
||||||
_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.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";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=
|
|
||||||
false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,
|
|
||||||
10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||
|
|
||||||
!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&
|
|
||||||
a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=
|
|
||||||
this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),
|
|
||||||
10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.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 a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,
|
|
||||||
(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!=
|
|
||||||
"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),
|
|
||||||
10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+
|
|
||||||
this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&
|
|
||||||
!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.left<g[0])e=g[0]+this.offset.click.left;
|
|
||||||
if(a.pageY-this.offset.click.top<g[1])h=g[1]+this.offset.click.top;if(a.pageX-this.offset.click.left>g[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.top<g[1]||h-this.offset.click.top>g[3])?h:!(h-this.offset.click.top<g[1])?h-b.grid[1]:h+b.grid[1]:h;e=b.grid[0]?this.originalPageX+Math.round((e-this.originalPageX)/
|
|
||||||
b.grid[0])*b.grid[0]:this.originalPageX;e=g?!(e-this.offset.click.left<g[0]||e-this.offset.click.left>g[2])?e:!(e-this.offset.click.left<g[0])?e-b.grid[0]:e+b.grid[0]:e}}return{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop()),left:e-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&d.browser.version<
|
|
||||||
526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove();this.helper=null;this.cancelHelperRemoval=false},_trigger:function(a,b,c){c=c||this._uiHash();d.ui.plugin.call(this,a,[b,c]);if(a=="drag")this.positionAbs=this._convertPositionTo("absolute");return d.Widget.prototype._trigger.call(this,a,b,
|
|
||||||
c)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}});d.extend(d.ui.draggable,{version:"1.8.14"});d.ui.plugin.add("draggable","connectToSortable",{start:function(a,b){var c=d(this).data("draggable"),f=c.options,e=d.extend({},b,{item:c.element});c.sortables=[];d(f.connectToSortable).each(function(){var h=d.data(this,"sortable");if(h&&!h.options.disabled){c.sortables.push({instance:h,shouldRevert:h.options.revert});
|
|
||||||
h.refreshPositions();h._trigger("activate",a,e)}})},stop:function(a,b){var c=d(this).data("draggable"),f=d.extend({},b,{item:c.element});d.each(c.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;c.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert)this.instance.options.revert=true;this.instance._mouseStop(a);this.instance.options.helper=this.instance.options._helper;c.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})}else{this.instance.cancelHelperRemoval=
|
|
||||||
false;this.instance._trigger("deactivate",a,f)}})},drag:function(a,b){var c=d(this).data("draggable"),f=this;d.each(c.sortables,function(){this.instance.positionAbs=c.positionAbs;this.instance.helperProportions=c.helperProportions;this.instance.offset.click=c.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=1;this.instance.currentItem=d(f).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",true);
|
|
||||||
this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return b.helper[0]};a.target=this.instance.currentItem[0];this.instance._mouseCapture(a,true);this.instance._mouseStart(a,true,true);this.instance.offset.click.top=c.offset.click.top;this.instance.offset.click.left=c.offset.click.left;this.instance.offset.parent.left-=c.offset.parent.left-this.instance.offset.parent.left;this.instance.offset.parent.top-=c.offset.parent.top-this.instance.offset.parent.top;
|
|
||||||
c._trigger("toSortable",a);c.dropped=this.instance.element;c.currentItem=c.element;this.instance.fromOutside=c}this.instance.currentItem&&this.instance._mouseDrag(a)}else if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",a,this.instance._uiHash(this.instance));this.instance._mouseStop(a,true);this.instance.options.helper=this.instance.options._helper;this.instance.currentItem.remove();this.instance.placeholder&&
|
|
||||||
this.instance.placeholder.remove();c._trigger("fromSortable",a);c.dropped=false}})}});d.ui.plugin.add("draggable","cursor",{start:function(){var a=d("body"),b=d(this).data("draggable").options;if(a.css("cursor"))b._cursor=a.css("cursor");a.css("cursor",b.cursor)},stop:function(){var a=d(this).data("draggable").options;a._cursor&&d("body").css("cursor",a._cursor)}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("opacity"))b._opacity=
|
|
||||||
a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){if(!c.axis||c.axis!=
|
|
||||||
"x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop+c.scrollSpeed;else if(a.pageY-b.overflowOffset.top<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop-c.scrollSpeed;if(!c.axis||c.axis!="y")if(b.overflowOffset.left+b.scrollParent[0].offsetWidth-a.pageX<c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft+c.scrollSpeed;else if(a.pageX-b.overflowOffset.left<
|
|
||||||
c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft-c.scrollSpeed}else{if(!c.axis||c.axis!="x")if(a.pageY-d(document).scrollTop()<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()-c.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()+c.scrollSpeed);if(!c.axis||c.axis!="y")if(a.pageX-d(document).scrollLeft()<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()-
|
|
||||||
c.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()+c.scrollSpeed)}f!==false&&d.ui.ddmanager&&!c.dropBehaviour&&d.ui.ddmanager.prepareOffsets(b,a)}});d.ui.plugin.add("draggable","snap",{start:function(){var a=d(this).data("draggable"),b=a.options;a.snapElements=[];d(b.snap.constructor!=String?b.snap.items||":data(draggable)":b.snap).each(function(){var c=d(this),f=c.offset();this!=a.element[0]&&a.snapElements.push({item:this,
|
|
||||||
width:c.outerWidth(),height:c.outerHeight(),top:f.top,left:f.left})})},drag:function(a,b){for(var c=d(this).data("draggable"),f=c.options,e=f.snapTolerance,h=b.offset.left,g=h+c.helperProportions.width,n=b.offset.top,o=n+c.helperProportions.height,i=c.snapElements.length-1;i>=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e<h&&h<l+e&&k-e<n&&n<m+e||j-e<h&&h<l+e&&k-e<o&&o<m+e||j-e<g&&g<l+e&&k-e<n&&n<m+e||j-e<g&&g<l+e&&k-e<o&&
|
|
||||||
o<m+e){if(f.snapMode!="inner"){var p=Math.abs(k-o)<=e,q=Math.abs(m-n)<=e,r=Math.abs(j-g)<=e,s=Math.abs(l-h)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:k-c.helperProportions.height,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:m,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:j-c.helperProportions.width}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:l}).left-c.margins.left}var t=
|
|
||||||
p||q||r||s;if(f.snapMode!="outer"){p=Math.abs(k-n)<=e;q=Math.abs(m-o)<=e;r=Math.abs(j-h)<=e;s=Math.abs(l-g)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:k,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:m-c.helperProportions.height,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:j}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:l-c.helperProportions.width}).left-c.margins.left}if(!c.snapElements[i].snapping&&
|
|
||||||
(p||q||r||s||t))c.options.snap.snap&&c.options.snap.snap.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[i].item}));c.snapElements[i].snapping=p||q||r||s||t}else{c.snapElements[i].snapping&&c.options.snap.release&&c.options.snap.release.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[i].item}));c.snapElements[i].snapping=false}}}});d.ui.plugin.add("draggable","stack",{start:function(){var a=d(this).data("draggable").options;a=d.makeArray(d(a.stack)).sort(function(c,f){return(parseInt(d(c).css("zIndex"),
|
|
||||||
10)||0)-(parseInt(d(f).css("zIndex"),10)||0)});if(a.length){var b=parseInt(a[0].style.zIndex)||0;d(a).each(function(c){this.style.zIndex=b+c});this[0].style.zIndex=b+a.length}}});d.ui.plugin.add("draggable","zIndex",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("zIndex"))b._zIndex=a.css("zIndex");a.css("zIndex",b.zIndex)},stop:function(a,b){a=d(this).data("draggable").options;a._zIndex&&d(b.helper).css("zIndex",a._zIndex)}})})(jQuery);
|
|
||||||
;/*
|
|
||||||
* jQuery UI Droppable 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Droppables
|
|
||||||
*
|
|
||||||
* Depends:
|
|
||||||
* jquery.ui.core.js
|
|
||||||
* jquery.ui.widget.js
|
|
||||||
* jquery.ui.mouse.js
|
|
||||||
* jquery.ui.draggable.js
|
|
||||||
*/
|
|
||||||
(function(d){d.widget("ui.droppable",{widgetEventPrefix:"drop",options:{accept:"*",activeClass:false,addClasses:true,greedy:false,hoverClass:false,scope:"default",tolerance:"intersect"},_create:function(){var a=this.options,b=a.accept;this.isover=0;this.isout=1;this.accept=d.isFunction(b)?b:function(c){return c.is(b)};this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight};d.ui.ddmanager.droppables[a.scope]=d.ui.ddmanager.droppables[a.scope]||[];d.ui.ddmanager.droppables[a.scope].push(this);
|
|
||||||
a.addClasses&&this.element.addClass("ui-droppable")},destroy:function(){for(var a=d.ui.ddmanager.droppables[this.options.scope],b=0;b<a.length;b++)a[b]==this&&a.splice(b,1);this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable");return this},_setOption:function(a,b){if(a=="accept")this.accept=d.isFunction(b)?b:function(c){return c.is(b)};d.Widget.prototype._setOption.apply(this,arguments)},_activate:function(a){var b=d.ui.ddmanager.current;this.options.activeClass&&
|
|
||||||
this.element.addClass(this.options.activeClass);b&&this._trigger("activate",a,this.ui(b))},_deactivate:function(a){var b=d.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass);b&&this._trigger("deactivate",a,this.ui(b))},_over:function(a){var b=d.ui.ddmanager.current;if(!(!b||(b.currentItem||b.element)[0]==this.element[0]))if(this.accept.call(this.element[0],b.currentItem||b.element)){this.options.hoverClass&&this.element.addClass(this.options.hoverClass);
|
|
||||||
this._trigger("over",a,this.ui(b))}},_out:function(a){var b=d.ui.ddmanager.current;if(!(!b||(b.currentItem||b.element)[0]==this.element[0]))if(this.accept.call(this.element[0],b.currentItem||b.element)){this.options.hoverClass&&this.element.removeClass(this.options.hoverClass);this._trigger("out",a,this.ui(b))}},_drop:function(a,b){var c=b||d.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return false;var e=false;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var g=
|
|
||||||
d.data(this,"droppable");if(g.options.greedy&&!g.options.disabled&&g.options.scope==c.options.scope&&g.accept.call(g.element[0],c.currentItem||c.element)&&d.ui.intersect(c,d.extend(g,{offset:g.element.offset()}),g.options.tolerance)){e=true;return false}});if(e)return false;if(this.accept.call(this.element[0],c.currentItem||c.element)){this.options.activeClass&&this.element.removeClass(this.options.activeClass);this.options.hoverClass&&this.element.removeClass(this.options.hoverClass);this._trigger("drop",
|
|
||||||
a,this.ui(c));return this.element}return false},ui:function(a){return{draggable:a.currentItem||a.element,helper:a.helper,position:a.position,offset:a.positionAbs}}});d.extend(d.ui.droppable,{version:"1.8.14"});d.ui.intersect=function(a,b,c){if(!b.offset)return false;var e=(a.positionAbs||a.position.absolute).left,g=e+a.helperProportions.width,f=(a.positionAbs||a.position.absolute).top,h=f+a.helperProportions.height,i=b.offset.left,k=i+b.proportions.width,j=b.offset.top,l=j+b.proportions.height;
|
|
||||||
switch(c){case "fit":return i<=e&&g<=k&&j<=f&&h<=l;case "intersect":return i<e+a.helperProportions.width/2&&g-a.helperProportions.width/2<k&&j<f+a.helperProportions.height/2&&h-a.helperProportions.height/2<l;case "pointer":return d.ui.isOver((a.positionAbs||a.position.absolute).top+(a.clickOffset||a.offset.click).top,(a.positionAbs||a.position.absolute).left+(a.clickOffset||a.offset.click).left,j,i,b.proportions.height,b.proportions.width);case "touch":return(f>=j&&f<=l||h>=j&&h<=l||f<j&&h>l)&&(e>=
|
|
||||||
i&&e<=k||g>=i&&g<=k||e<i&&g>k);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f<c.length;f++)if(!(c[f].options.disabled||a&&!c[f].accept.call(c[f].element[0],a.currentItem||a.element))){for(var h=0;h<g.length;h++)if(g[h]==c[f].element[0]){c[f].proportions.height=0;continue a}c[f].visible=c[f].element.css("display")!=
|
|
||||||
"none";if(c[f].visible){e=="mousedown"&&c[f]._activate.call(c[f],b);c[f].offset=c[f].element.offset();c[f].proportions={width:c[f].element[0].offsetWidth,height:c[f].element[0].offsetHeight}}}},drop:function(a,b){var c=false;d.each(d.ui.ddmanager.droppables[a.options.scope]||[],function(){if(this.options){if(!this.options.disabled&&this.visible&&d.ui.intersect(a,this,this.options.tolerance))c=c||this._drop.call(this,b);if(!this.options.disabled&&this.visible&&this.accept.call(this.element[0],a.currentItem||
|
|
||||||
a.element)){this.isout=1;this.isover=0;this._deactivate.call(this,b)}}});return c},dragStart:function(a,b){a.element.parentsUntil("body").bind("scroll.droppable",function(){a.options.refreshPositions||d.ui.ddmanager.prepareOffsets(a,b)})},drag:function(a,b){a.options.refreshPositions&&d.ui.ddmanager.prepareOffsets(a,b);d.each(d.ui.ddmanager.droppables[a.options.scope]||[],function(){if(!(this.options.disabled||this.greedyChild||!this.visible)){var c=d.ui.intersect(a,this,this.options.tolerance);if(c=
|
|
||||||
!c&&this.isover==1?"isout":c&&this.isover==0?"isover":null){var e;if(this.options.greedy){var g=this.element.parents(":data(droppable):eq(0)");if(g.length){e=d.data(g[0],"droppable");e.greedyChild=c=="isover"?1:0}}if(e&&c=="isover"){e.isover=0;e.isout=1;e._out.call(e,b)}this[c]=1;this[c=="isout"?"isover":"isout"]=0;this[c=="isover"?"_over":"_out"].call(this,b);if(e&&c=="isout"){e.isout=0;e.isover=1;e._over.call(e,b)}}}})},dragStop:function(a,b){a.element.parentsUntil("body").unbind("scroll.droppable");
|
|
||||||
a.options.refreshPositions||d.ui.ddmanager.prepareOffsets(a,b)}}})(jQuery);
|
|
||||||
;/*
|
|
||||||
* jQuery UI Autocomplete 1.8.14
|
|
||||||
*
|
|
||||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* http://docs.jquery.com/UI/Autocomplete
|
|
||||||
*
|
|
||||||
* Depends:
|
|
||||||
* jquery.ui.core.js
|
|
||||||
* jquery.ui.widget.js
|
|
||||||
* jquery.ui.position.js
|
|
||||||
*/
|
|
||||||
(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.attr("readonly"))){g=
|
|
||||||
false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=
|
|
||||||
a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};
|
|
||||||
this.menu=d("<ul></ul>").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&&
|
|
||||||
a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");
|
|
||||||
d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&&
|
|
||||||
b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source=
|
|
||||||
this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)!==false)return this._search(a)},_search:function(a){this.pending++;this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(!this.options.disabled&&a&&a.length){a=this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();
|
|
||||||
this.pending--;this.pending||this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this.menu.element.hide();this.menu.deactivate();this._trigger("close",a)}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return d.map(a,function(b){if(typeof b==="string")return{label:b,value:b};return d.extend({label:b.label||
|
|
||||||
b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();b.show();this._resizeMenu();b.position(d.extend({of:this.element},this.options.position));this.options.autoFocus&&this.menu.next(new d.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth(),this.element.outerWidth()))},_renderMenu:function(a,b){var g=this;
|
|
||||||
d.each(b,function(c,f){g._renderItem(a,f)})},_renderItem:function(a,b){return d("<li></li>").data("item.autocomplete",b).append(d("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,
|
|
||||||
"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery);
|
|
||||||
(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex",
|
|
||||||
-1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");
|
|
||||||
this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b,
|
|
||||||
this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
|
|
||||||
this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
|
|
||||||
this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[d.fn.prop?"prop":"attr"]("scrollHeight")},select:function(e){this._trigger("selected",e,{item:this.active})}})})(jQuery);
|
|
||||||
;
|
|
||||||
20
app/assets/javascripts/lib/react.js
vendored
12
app/assets/javascripts/lib/tweenjs.min.js
vendored
|
|
@ -1,218 +0,0 @@
|
||||||
class OutfitViewer extends HTMLElement {
|
|
||||||
#internals;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.#internals = this.attachInternals(); // for CSS `:state()`
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// The `<outfit-layer>` is connected to the DOM right before its
|
|
||||||
// children are. So, to engage with the children, wait a tick!
|
|
||||||
setTimeout(() => this.#connectToChildren(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#connectToChildren() {
|
|
||||||
const playPauseToggle = document.querySelector(".play-pause-toggle");
|
|
||||||
|
|
||||||
// Read our initial playing state from the toggle, and subscribe to changes.
|
|
||||||
this.#setIsPlaying(playPauseToggle.checked);
|
|
||||||
playPauseToggle.addEventListener("change", () => {
|
|
||||||
this.#setIsPlaying(playPauseToggle.checked);
|
|
||||||
this.#setIsPlayingCookie(playPauseToggle.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tell the CSS our first frame has rendered, which we use for loading
|
|
||||||
// state transitions.
|
|
||||||
this.#internals.states.add("after-first-frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
#setIsPlaying(isPlaying) {
|
|
||||||
// TODO: Listen for changes to the child list, and add `playing` when new
|
|
||||||
// nodes arrive, if playing.
|
|
||||||
const thirtyDays = 60 * 60 * 24 * 30;
|
|
||||||
if (isPlaying) {
|
|
||||||
this.#internals.states.add("playing");
|
|
||||||
for (const layer of this.querySelectorAll("outfit-layer")) {
|
|
||||||
layer.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.#internals.states.delete("playing");
|
|
||||||
for (const layer of this.querySelectorAll("outfit-layer")) {
|
|
||||||
layer.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#setIsPlayingCookie(isPlaying) {
|
|
||||||
const thirtyDays = 60 * 60 * 24 * 30;
|
|
||||||
const value = isPlaying ? "true" : "false";
|
|
||||||
document.cookie = `DTIOutfitViewerIsPlaying=${value};max-age=${thirtyDays}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OutfitLayer extends HTMLElement {
|
|
||||||
#internals;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.#internals = this.attachInternals();
|
|
||||||
|
|
||||||
// An <outfit-layer> starts in the loading state, and then might very
|
|
||||||
// quickly decide it's not after `#connectToChildren`. This is to prevent a
|
|
||||||
// flash of *non*-loading state, when a new layer loads in. (e.g. In the
|
|
||||||
// time between our parent <turbo-frame> loading, which shows the loading
|
|
||||||
// spinner; and us being marked `:state(loading)`, which shows the loading
|
|
||||||
// spinner; we don't want the loading spinner to do its usual *immediate*
|
|
||||||
// total fade-out; then have to fade back in again, on the usual delay.)
|
|
||||||
this.#setStatus("loading");
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
setTimeout(() => this.#connectToChildren(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
// When this `<outfit-layer>` leaves the DOM, stop listening for iframe
|
|
||||||
// messages, if we were.
|
|
||||||
window.removeEventListener("message", this.#onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
play() {
|
|
||||||
this.#sendMessageToIframe({ type: "play" });
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this.#sendMessageToIframe({ type: "pause" });
|
|
||||||
}
|
|
||||||
|
|
||||||
#connectToChildren() {
|
|
||||||
const image = this.querySelector("img");
|
|
||||||
const iframe = this.querySelector("iframe");
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
// If this is an image layer, track its loading state by listening
|
|
||||||
// to the load/error events, and initialize based on whether it's
|
|
||||||
// already `complete` (which it can be if it loaded from cache).
|
|
||||||
this.#setStatus(image.complete ? "loaded" : "loading");
|
|
||||||
image.addEventListener("load", () => this.#setStatus("loaded"));
|
|
||||||
image.addEventListener("error", () => this.#setStatus("error"));
|
|
||||||
} else if (iframe) {
|
|
||||||
this.iframe = iframe;
|
|
||||||
|
|
||||||
// Initialize status to `loading`, and asynchronously request a
|
|
||||||
// status message from the iframe if it managed to load before this
|
|
||||||
// triggers (impressive, but I think I've seen it happen!). Then,
|
|
||||||
// wait for messages or error events from the iframe to update
|
|
||||||
// status further if needed.
|
|
||||||
this.#setStatus("loading");
|
|
||||||
this.#sendMessageToIframe({ type: "requestStatus" });
|
|
||||||
window.addEventListener("message", (m) => this.#onMessage(m));
|
|
||||||
this.iframe.addEventListener("error", () => this.#setStatus("error"));
|
|
||||||
} else {
|
|
||||||
console.warn(`<outfit-layer> contained no image or iframe: `, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#onMessage({ source, data }) {
|
|
||||||
// Ignore messages that aren't from *our* frame.
|
|
||||||
if (source !== this.iframe.contentWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the incoming status message, then set our status to match.
|
|
||||||
if (data.type === "status") {
|
|
||||||
if (data.status === "loaded") {
|
|
||||||
this.#setStatus("loaded");
|
|
||||||
this.#setHasAnimations(data.hasAnimations);
|
|
||||||
} else if (data.status === "error") {
|
|
||||||
this.#setStatus("error");
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`<outfit-layer> got unexpected status: ` +
|
|
||||||
JSON.stringify(data.status),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`<outfit-layer> got unexpected message: ` + JSON.stringify(data),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the status value that the CSS `:state()` selector will match.
|
|
||||||
* For example, when loading, `:state(loading)` matches this element.
|
|
||||||
*/
|
|
||||||
#setStatus(newStatus) {
|
|
||||||
this.#internals.states.delete("loading");
|
|
||||||
this.#internals.states.delete("loaded");
|
|
||||||
this.#internals.states.delete("error");
|
|
||||||
this.#internals.states.add(newStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether CSS selector `:state(has-animations)` matches this element.
|
|
||||||
*/
|
|
||||||
#setHasAnimations(hasAnimations) {
|
|
||||||
if (hasAnimations) {
|
|
||||||
this.#internals.states.add("has-animations");
|
|
||||||
} else {
|
|
||||||
this.#internals.states.delete("has-animations");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendMessageToIframe(message) {
|
|
||||||
// If we have no frame or it hasn't loaded, ignore this message.
|
|
||||||
if (this.iframe == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.iframe.contentWindow == null) {
|
|
||||||
console.debug(
|
|
||||||
`Ignoring message, frame not loaded yet: `,
|
|
||||||
this.iframe,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The frame is sandboxed (origin == null), so send to Any origin.
|
|
||||||
this.iframe.contentWindow.postMessage(message, "*");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("outfit-viewer", OutfitViewer);
|
|
||||||
customElements.define("outfit-layer", OutfitLayer);
|
|
||||||
|
|
||||||
// Morph turbo-frames on this page, to reuse asset nodes when we want to—very
|
|
||||||
// important for movies!—but ensure that it *doesn't* do its usual behavior of
|
|
||||||
// aggressively reusing existing <outfit-layer> nodes for entirely different
|
|
||||||
// assets. (It's a lot clearer for managing the loading state, and not showing
|
|
||||||
// old incorrect layers!) (We also tried using `id` to enforce this… no luck.)
|
|
||||||
function morphWithOutfitLayers(currentElement, newElement) {
|
|
||||||
Idiomorph.morph(currentElement, newElement.innerHTML, {
|
|
||||||
morphStyle: "innerHTML",
|
|
||||||
callbacks: {
|
|
||||||
beforeNodeMorphed: (currentNode, newNode) => {
|
|
||||||
// If Idiomorph wants to transform an <outfit-layer> to
|
|
||||||
// have a different data-asset-id attribute, we replace
|
|
||||||
// the node ourselves and abort the morph.
|
|
||||||
if (
|
|
||||||
newNode.tagName === "OUTFIT-LAYER" &&
|
|
||||||
newNode.getAttribute("data-asset-id") !==
|
|
||||||
currentNode.getAttribute("data-asset-id")
|
|
||||||
) {
|
|
||||||
currentNode.replaceWith(newNode);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addEventListener("turbo:before-frame-render", (event) => {
|
|
||||||
// Rather than enforce Idiomorph must be loaded, let's just be resilient
|
|
||||||
// and only bother if we have it. (Replacing content is not *that* bad!)
|
|
||||||
if (typeof Idiomorph !== "undefined") {
|
|
||||||
event.detail.render = (a, b) => morphWithOutfitLayers(a, b);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
(function () {
|
|
||||||
function petImage(id, size) {
|
|
||||||
return "https://pets.neopets.com/" + id + "/1/" + size + ".png";
|
|
||||||
}
|
|
||||||
|
|
||||||
var PetQuery = {},
|
|
||||||
query_string = document.location.hash || document.location.search;
|
|
||||||
|
|
||||||
for (const [key, value] of new URLSearchParams(query_string).entries()) {
|
|
||||||
PetQuery[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PetQuery.name) {
|
|
||||||
if (PetQuery.species && PetQuery.color) {
|
|
||||||
$("#pet-query-notice-template")
|
|
||||||
.tmpl({
|
|
||||||
pet_name: PetQuery.name,
|
|
||||||
pet_image_url: petImage("cpn/" + PetQuery.name, 1),
|
|
||||||
})
|
|
||||||
.prependTo("#container");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var preview_el = $("#pet-preview"),
|
|
||||||
img_el = preview_el.find("img"),
|
|
||||||
response_el = preview_el.find("span");
|
|
||||||
|
|
||||||
var defaultPreviewUrl = img_el.attr("src");
|
|
||||||
|
|
||||||
preview_el.click(function () {
|
|
||||||
Preview.Job.current.visit();
|
|
||||||
});
|
|
||||||
|
|
||||||
var Preview = {
|
|
||||||
clear: function () {
|
|
||||||
if (typeof Preview.Job.fallback != "undefined")
|
|
||||||
Preview.Job.fallback.setAsCurrent();
|
|
||||||
},
|
|
||||||
displayLoading: function () {
|
|
||||||
preview_el.addClass("loading");
|
|
||||||
response_el.text("Loading...");
|
|
||||||
},
|
|
||||||
failed: function () {
|
|
||||||
preview_el.addClass("hidden");
|
|
||||||
},
|
|
||||||
notFound: function (key, options) {
|
|
||||||
Preview.failed();
|
|
||||||
response_el.empty();
|
|
||||||
$("#preview-" + key + "-template")
|
|
||||||
.tmpl(options)
|
|
||||||
.appendTo(response_el);
|
|
||||||
},
|
|
||||||
updateWithName: function (name_el) {
|
|
||||||
var name = name_el.val(),
|
|
||||||
job;
|
|
||||||
if (name) {
|
|
||||||
currentName = name;
|
|
||||||
if (!Preview.Job.current || name != Preview.Job.current.name) {
|
|
||||||
job = new Preview.Job.Name(name);
|
|
||||||
job.setAsCurrent();
|
|
||||||
Preview.displayLoading();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Preview.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadFeature() {
|
|
||||||
$.getJSON("/donations/features", function (features) {
|
|
||||||
if (features.length > 0) {
|
|
||||||
var feature = features[Math.floor(Math.random() * features.length)];
|
|
||||||
Preview.Job.fallback = new Preview.Job.Feature(feature);
|
|
||||||
if (!Preview.Job.current) {
|
|
||||||
Preview.Job.fallback.setAsCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFeature();
|
|
||||||
|
|
||||||
Preview.Job = function (key, base) {
|
|
||||||
var job = this,
|
|
||||||
quality = 2;
|
|
||||||
job.loading = false;
|
|
||||||
|
|
||||||
function getImageSrc() {
|
|
||||||
if (base === "cp" || base === "cpn") {
|
|
||||||
return petImage(base + "/" + key, quality);
|
|
||||||
} else if (base === "url") {
|
|
||||||
return key;
|
|
||||||
} else {
|
|
||||||
throw new Error("unrecognized image base " + base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
job.loading = true;
|
|
||||||
img_el.attr("src", getImageSrc());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.increaseQualityIfPossible = function () {
|
|
||||||
if (quality == 2) {
|
|
||||||
quality = 4;
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setAsCurrent = function () {
|
|
||||||
Preview.Job.current = job;
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.notFound = function () {
|
|
||||||
Preview.notFound("pet-not-found");
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Preview.Job.Name = function (name) {
|
|
||||||
this.name = name;
|
|
||||||
if (name.startsWith("@")) {
|
|
||||||
// This is an image hash "pet name".
|
|
||||||
Preview.Job.apply(this, [name.substr(1), "cp"]);
|
|
||||||
} else {
|
|
||||||
// This is a normal pet name.
|
|
||||||
Preview.Job.apply(this, [name, "cpn"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visit = function () {
|
|
||||||
$(".main-pet-name").val(this.name).closest("form").submit();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Preview.Job.Hash = function (hash, form) {
|
|
||||||
Preview.Job.apply(this, [hash, "cp"]);
|
|
||||||
|
|
||||||
this.visit = function () {
|
|
||||||
window.location =
|
|
||||||
"/wardrobe?color=" +
|
|
||||||
form.find(".color").val() +
|
|
||||||
"&species=" +
|
|
||||||
form.find(".species").val();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Preview.Job.Feature = function (feature) {
|
|
||||||
Preview.Job.apply(this, [feature.outfit_image_url, "url"]);
|
|
||||||
this.name = "Thanks for donating, " + feature.donor_name + "!"; // TODO: i18n
|
|
||||||
|
|
||||||
this.visit = function () {
|
|
||||||
window.location = "/donate";
|
|
||||||
};
|
|
||||||
|
|
||||||
this.notFound = function () {
|
|
||||||
// The outfit thumbnail hasn't generated or is missing or something.
|
|
||||||
// Let's fall back to a boring image for now.
|
|
||||||
var boring = new Preview.Job.Feature({
|
|
||||||
donor_name: feature.donor_name,
|
|
||||||
outfit_image_url: defaultPreviewUrl,
|
|
||||||
});
|
|
||||||
boring.setAsCurrent();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
var previewWithNameTimeout;
|
|
||||||
|
|
||||||
var name_el = $(".main-pet-name");
|
|
||||||
name_el.val(PetQuery.name);
|
|
||||||
Preview.updateWithName(name_el);
|
|
||||||
|
|
||||||
name_el.keyup(function () {
|
|
||||||
if (previewWithNameTimeout && Preview.Job.current) {
|
|
||||||
clearTimeout(previewWithNameTimeout);
|
|
||||||
Preview.Job.current.loading = false;
|
|
||||||
}
|
|
||||||
var name_el = $(this);
|
|
||||||
previewWithNameTimeout = setTimeout(function () {
|
|
||||||
Preview.updateWithName(name_el);
|
|
||||||
}, 250);
|
|
||||||
});
|
|
||||||
|
|
||||||
img_el
|
|
||||||
.load(function () {
|
|
||||||
if (Preview.Job.current.loading) {
|
|
||||||
Preview.Job.loading = false;
|
|
||||||
Preview.Job.current.increaseQualityIfPossible();
|
|
||||||
preview_el
|
|
||||||
.removeClass("loading")
|
|
||||||
.removeClass("hidden")
|
|
||||||
.addClass("loaded");
|
|
||||||
response_el.text(Preview.Job.current.name);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.error(function () {
|
|
||||||
if (Preview.Job.current.loading) {
|
|
||||||
Preview.Job.loading = false;
|
|
||||||
Preview.Job.current.notFound();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".species, .color").change(function () {
|
|
||||||
var type = {},
|
|
||||||
nameComponents = {};
|
|
||||||
var form = $(this).closest("form");
|
|
||||||
form.find("select").each(function () {
|
|
||||||
var el = $(this),
|
|
||||||
selectedEl = el.children(":selected"),
|
|
||||||
key = el.attr("name");
|
|
||||||
type[key] = selectedEl.val();
|
|
||||||
nameComponents[key] = selectedEl.text();
|
|
||||||
});
|
|
||||||
name = nameComponents.color + " " + nameComponents.species;
|
|
||||||
Preview.displayLoading();
|
|
||||||
$.ajax({
|
|
||||||
url:
|
|
||||||
"/species/" +
|
|
||||||
type.species +
|
|
||||||
"/colors/" +
|
|
||||||
type.color +
|
|
||||||
"/pet_type.json",
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
var job;
|
|
||||||
if (data) {
|
|
||||||
job = new Preview.Job.Hash(data.image_hash, form);
|
|
||||||
job.name = name;
|
|
||||||
job.setAsCurrent();
|
|
||||||
} else {
|
|
||||||
Preview.notFound("pet-type-not-found", {
|
|
||||||
color_name: nameComponents.color,
|
|
||||||
species_name: nameComponents.species,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".load-pet-to-wardrobe").submit(function (e) {
|
|
||||||
if ($(this).find(".main-pet-name").val() === "" && Preview.Job.current) {
|
|
||||||
e.preventDefault();
|
|
||||||
Preview.Job.current.visit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#latest-contribution-created-at").timeago();
|
|
||||||
})();
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
/* Bulk pets form */
|
|
||||||
(function () {
|
|
||||||
var form = $("#bulk-pets-form"),
|
|
||||||
queue_el = form.find("ul"),
|
|
||||||
names_el = form.find("textarea"),
|
|
||||||
add_el = $("#bulk-pets-form-add"),
|
|
||||||
clear_el = $("#bulk-pets-form-clear"),
|
|
||||||
bulk_load_queue;
|
|
||||||
|
|
||||||
$(document.body).addClass("js");
|
|
||||||
|
|
||||||
function petThumbnailUrl(pet_name) {
|
|
||||||
// if first character is "@", use the hash url
|
|
||||||
if (pet_name[0] == "@") {
|
|
||||||
return "https://pets.neopets.com/cp/" + pet_name.substr(1) + "/1/1.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
bulk_load_queue = new (function BulkLoadQueue() {
|
|
||||||
var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30;
|
|
||||||
var RECENTLY_SENT_MAX = 3;
|
|
||||||
var pets = [],
|
|
||||||
url = form.attr("action") + ".json",
|
|
||||||
recently_sent_count = 0,
|
|
||||||
loading = false;
|
|
||||||
|
|
||||||
function Pet(name) {
|
|
||||||
var el = $("#bulk-pets-submission-template")
|
|
||||||
.tmpl({ pet_name: name, pet_thumbnail: petThumbnailUrl(name) })
|
|
||||||
.appendTo(queue_el);
|
|
||||||
|
|
||||||
this.load = function () {
|
|
||||||
el.removeClass("waiting").addClass("loading");
|
|
||||||
var response_el = el.find("span.response");
|
|
||||||
pets.shift();
|
|
||||||
loading = true;
|
|
||||||
$.ajax({
|
|
||||||
complete: function (data) {
|
|
||||||
loading = false;
|
|
||||||
loadNextIfReady();
|
|
||||||
},
|
|
||||||
data: { name: name },
|
|
||||||
dataType: "json",
|
|
||||||
error: function (xhr) {
|
|
||||||
el.removeClass("loading").addClass("failed");
|
|
||||||
response_el.text(xhr.responseText);
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
var points = data.points;
|
|
||||||
el.removeClass("loading").addClass("loaded");
|
|
||||||
$("#bulk-pets-submission-success-template")
|
|
||||||
.tmpl({ points: points })
|
|
||||||
.appendTo(response_el);
|
|
||||||
},
|
|
||||||
type: "post",
|
|
||||||
url: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
recently_sent_count++;
|
|
||||||
setTimeout(function () {
|
|
||||||
recently_sent_count--;
|
|
||||||
loadNextIfReady();
|
|
||||||
}, RECENTLY_SENT_INTERVAL_IN_SECONDS * 1000);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.add = function (name) {
|
|
||||||
name = name.replace(/^\s+|\s+$/g, "");
|
|
||||||
if (name.length) {
|
|
||||||
var pet = new Pet(name);
|
|
||||||
pets.push(pet);
|
|
||||||
if (pets.length == 1) loadNextIfReady();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadNextIfReady() {
|
|
||||||
if (!loading && recently_sent_count < RECENTLY_SENT_MAX && pets.length) {
|
|
||||||
pets[0].load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
names_el.keyup(function () {
|
|
||||||
var names = this.value.split("\n"),
|
|
||||||
x = names.length - 1,
|
|
||||||
i,
|
|
||||||
name;
|
|
||||||
for (i = 0; i < x; i++) {
|
|
||||||
bulk_load_queue.add(names[i]);
|
|
||||||
}
|
|
||||||
this.value = x >= 0 ? names[x] : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
add_el.click(function () {
|
|
||||||
bulk_load_queue.add(names_el.val());
|
|
||||||
names_el.val("");
|
|
||||||
});
|
|
||||||
|
|
||||||
clear_el.click(function () {
|
|
||||||
queue_el.children("li.loaded, li.failed").remove();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,356 +0,0 @@
|
||||||
const canvas = document.getElementById("asset-canvas");
|
|
||||||
const libraryScript = document.getElementById("canvas-movie-library");
|
|
||||||
const libraryUrl = libraryScript.getAttribute("src");
|
|
||||||
|
|
||||||
// Read the asset ID from the URL, as an extra hint of what asset we're
|
|
||||||
// logging for. (This is helpful when there's a lot of assets animating!)
|
|
||||||
const assetId = document.location.pathname.split("/").at(-1);
|
|
||||||
const logPrefix = `[${assetId}] `.padEnd(9);
|
|
||||||
|
|
||||||
// State for controlling the movie.
|
|
||||||
let loadingStatus = "loading";
|
|
||||||
let playingStatus = getInitialPlayingStatus();
|
|
||||||
|
|
||||||
// State for loading the movie.
|
|
||||||
let library = null;
|
|
||||||
let movieClip = null;
|
|
||||||
let stage = null;
|
|
||||||
|
|
||||||
// State for animating the movie.
|
|
||||||
let frameRequestId = null;
|
|
||||||
let lastFrameTime = null;
|
|
||||||
let lastLogTime = null;
|
|
||||||
let numFramesSinceLastLog = 0;
|
|
||||||
|
|
||||||
// State for error reporting.
|
|
||||||
let hasLoggedRenderError = false;
|
|
||||||
|
|
||||||
function loadImage(src) {
|
|
||||||
const image = new Image();
|
|
||||||
image.crossOrigin = "anonymous";
|
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
|
||||||
image.onload = () => {
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
image.onerror = () => {
|
|
||||||
reject(new Error(`Failed to load image: ${JSON.stringify(src)}`));
|
|
||||||
};
|
|
||||||
image.src = src;
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLibrary() {
|
|
||||||
if (Object.keys(window.AdobeAn?.compositions || {}).length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Movie library ${libraryUrl} did not add a composition to window.AdobeAn.compositions.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const [compositionId, composition] = Object.entries(
|
|
||||||
window.AdobeAn.compositions,
|
|
||||||
)[0];
|
|
||||||
if (Object.keys(window.AdobeAn.compositions).length > 1) {
|
|
||||||
console.warn(
|
|
||||||
`Grabbing composition ${compositionId}, but there are >1 here: `,
|
|
||||||
Object.keys(window.AdobeAn.compositions).length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
delete window.AdobeAn.compositions[compositionId];
|
|
||||||
|
|
||||||
const library = composition.getLibrary();
|
|
||||||
|
|
||||||
// One more loading step as part of loading this library is loading the
|
|
||||||
// images it uses for sprites.
|
|
||||||
//
|
|
||||||
// TODO: I guess the manifest has these too, so we could put them in preload
|
|
||||||
// meta tags to get them here faster?
|
|
||||||
const librarySrcDir = libraryUrl.split("/").slice(0, -1).join("/");
|
|
||||||
const manifestImages = new Map(
|
|
||||||
library.properties.manifest.map(({ id, src }) => [
|
|
||||||
id,
|
|
||||||
loadImage(librarySrcDir + "/" + src),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(manifestImages.values());
|
|
||||||
|
|
||||||
// Finally, once we have the images loaded, the library object expects us to
|
|
||||||
// mutate it (!) to give it the actual image and sprite sheet objects from
|
|
||||||
// the loaded images. That's how the MovieClip's internal JS objects will
|
|
||||||
// access the loaded data!
|
|
||||||
const images = composition.getImages();
|
|
||||||
for (const [id, image] of manifestImages.entries()) {
|
|
||||||
images[id] = await image;
|
|
||||||
}
|
|
||||||
const spriteSheets = composition.getSpriteSheet();
|
|
||||||
for (const { name, frames } of library.ssMetadata) {
|
|
||||||
const image = await manifestImages.get(name);
|
|
||||||
spriteSheets[name] = new window.createjs.SpriteSheet({
|
|
||||||
images: [image],
|
|
||||||
frames,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return library;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMovieClip(library) {
|
|
||||||
let constructorName;
|
|
||||||
try {
|
|
||||||
const fileName = decodeURI(libraryUrl).split("/").pop();
|
|
||||||
const fileNameWithoutExtension = fileName.split(".")[0];
|
|
||||||
constructorName = fileNameWithoutExtension.replace(/[ -]/g, "");
|
|
||||||
if (constructorName.match(/^[0-9]/)) {
|
|
||||||
constructorName = "_" + constructorName;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(
|
|
||||||
`Movie libraryUrl ${JSON.stringify(libraryUrl)} did not match expected ` +
|
|
||||||
`format: ${e.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LibraryMovieClipConstructor = library[constructorName];
|
|
||||||
if (!LibraryMovieClipConstructor) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected JS movie library ${libraryUrl} to contain a constructor ` +
|
|
||||||
`named ${constructorName}, but it did not: ${Object.keys(library)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const movieClip = new LibraryMovieClipConstructor();
|
|
||||||
|
|
||||||
return movieClip;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStage() {
|
|
||||||
try {
|
|
||||||
stage.update();
|
|
||||||
} catch (e) {
|
|
||||||
// If rendering the frame fails, log it and proceed. If it's an
|
|
||||||
// animation, then maybe the next frame will work? Also alert the user,
|
|
||||||
// just as an FYI. (This is pretty uncommon, so I'm not worried about
|
|
||||||
// being noisy!)
|
|
||||||
if (!hasLoggedRenderError) {
|
|
||||||
console.error(`Error rendering movie clip ${libraryUrl}`, e);
|
|
||||||
// TODO: Inform user about the failure
|
|
||||||
hasLoggedRenderError = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCanvasDimensions() {
|
|
||||||
// Set the canvas's internal dimensions to be higher, if the device has high
|
|
||||||
// DPI. Scale the movie clip to match, too.
|
|
||||||
const internalWidth = canvas.offsetWidth * window.devicePixelRatio;
|
|
||||||
const internalHeight = canvas.offsetHeight * window.devicePixelRatio;
|
|
||||||
canvas.width = internalWidth;
|
|
||||||
canvas.height = internalHeight;
|
|
||||||
movieClip.scaleX = internalWidth / library.properties.width;
|
|
||||||
movieClip.scaleY = internalHeight / library.properties.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startMovie() {
|
|
||||||
// Load the movie's library (from the JS file already run), and use it to
|
|
||||||
// build a movie clip.
|
|
||||||
library = await getLibrary();
|
|
||||||
movieClip = buildMovieClip(library);
|
|
||||||
|
|
||||||
updateCanvasDimensions();
|
|
||||||
|
|
||||||
if (canvas.getContext("2d") == null) {
|
|
||||||
console.warn(`Out of memory, can't use canvas for ${libraryUrl}.`);
|
|
||||||
// TODO: "Too many animations!"
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stage = new window.createjs.Stage(canvas);
|
|
||||||
stage.addChild(movieClip);
|
|
||||||
updateStage();
|
|
||||||
|
|
||||||
loadingStatus = "loaded";
|
|
||||||
canvas.setAttribute("data-status", "loaded");
|
|
||||||
|
|
||||||
updateAnimationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAnimationState() {
|
|
||||||
const shouldRunAnimations =
|
|
||||||
loadingStatus === "loaded" && playingStatus === "playing";
|
|
||||||
|
|
||||||
if (shouldRunAnimations && frameRequestId == null) {
|
|
||||||
lastFrameTime = document.timeline.currentTime;
|
|
||||||
lastLogTime = document.timeline.currentTime;
|
|
||||||
numFramesSinceLastLog = 0;
|
|
||||||
documentHiddenSinceLastFrame = document.hidden;
|
|
||||||
frameRequestId = requestAnimationFrame(onAnimationFrame);
|
|
||||||
} else if (!shouldRunAnimations && frameRequestId != null) {
|
|
||||||
cancelAnimationFrame(frameRequestId);
|
|
||||||
lastFrameTime = null;
|
|
||||||
lastLogTime = null;
|
|
||||||
numFramesSinceLastLog = 0;
|
|
||||||
documentHiddenSinceLastFrame = false;
|
|
||||||
frameRequestId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAnimationFrame() {
|
|
||||||
const targetFps = library.properties.fps;
|
|
||||||
const msPerFrame = 1000 / targetFps;
|
|
||||||
const msSinceLastFrame = document.timeline.currentTime - lastFrameTime;
|
|
||||||
const msSinceLastLog = document.timeline.currentTime - lastLogTime;
|
|
||||||
|
|
||||||
// If it takes too long to render a frame, cancel the movie, on the
|
|
||||||
// assumption that we're riding the CPU too hard. (Some movies do this!)
|
|
||||||
//
|
|
||||||
// But note that, if the page is hidden (e.g. the window is not visible),
|
|
||||||
// it's normal for the browser to pause animations. So, if we detected that
|
|
||||||
// the document became hidden between this frame and the last, no
|
|
||||||
// intervention is necesary.
|
|
||||||
if (msSinceLastFrame >= 2000 && !documentHiddenSinceLastFrame) {
|
|
||||||
pause();
|
|
||||||
console.warn(`Paused movie for taking too long: ${msSinceLastFrame}ms`);
|
|
||||||
// TODO: Display message about low FPS, and sync up to the parent.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msSinceLastFrame >= msPerFrame) {
|
|
||||||
updateStage();
|
|
||||||
lastFrameTime = document.timeline.currentTime;
|
|
||||||
|
|
||||||
// If we're a little bit late to this frame, probably because the frame
|
|
||||||
// rate isn't an even divisor of 60 FPS, backdate it to what the ideal time
|
|
||||||
// for this frame *would* have been. (For example, without this tweak, a
|
|
||||||
// 24 FPS animation like the Floating Negg Faerie actually runs at 20 FPS,
|
|
||||||
// because it wants to run every 41.66ms, but a 60 FPS browser checks in
|
|
||||||
// every 16.66ms, so the best it can do is 50ms. With this tweak, we can
|
|
||||||
// *pretend* we ran at 41.66ms, so that the next frame timing correctly
|
|
||||||
// takes the extra 9.33ms into account.)
|
|
||||||
const msFrameDelay = msSinceLastFrame - msPerFrame;
|
|
||||||
if (msFrameDelay < msPerFrame) {
|
|
||||||
lastFrameTime -= msFrameDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
numFramesSinceLastLog++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msSinceLastLog >= 5000) {
|
|
||||||
const fps = numFramesSinceLastLog / (msSinceLastLog / 1000);
|
|
||||||
console.debug(`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`);
|
|
||||||
lastLogTime = document.timeline.currentTime;
|
|
||||||
numFramesSinceLastLog = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
frameRequestId = requestAnimationFrame(onAnimationFrame);
|
|
||||||
documentHiddenSinceLastFrame = document.hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If `document.hidden` becomes true at any point, log it for the next
|
|
||||||
// animation frame. (The next frame will reset the state, as will starting or
|
|
||||||
// stopping the animation.)
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
documentHiddenSinceLastFrame = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function play() {
|
|
||||||
playingStatus = "playing";
|
|
||||||
updateAnimationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause() {
|
|
||||||
playingStatus = "paused";
|
|
||||||
updateAnimationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialPlayingStatus() {
|
|
||||||
const params = new URLSearchParams(document.location.search);
|
|
||||||
if (params.has("playing")) {
|
|
||||||
return "playing";
|
|
||||||
} else {
|
|
||||||
return "paused";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively scans the given MovieClip (or child createjs node), to see if
|
|
||||||
* there are any animated areas.
|
|
||||||
*/
|
|
||||||
function hasAnimations(createjsNode) {
|
|
||||||
return (
|
|
||||||
// Some nodes have simple animation frames.
|
|
||||||
createjsNode.totalFrames > 1 ||
|
|
||||||
// Tweens are a form of animation that can happen separately from frames.
|
|
||||||
// They expect timer ticks to happen, and they change the scene accordingly.
|
|
||||||
createjsNode?.timeline?.tweens?.length >= 1 ||
|
|
||||||
// And some nodes have _children_ that are animated.
|
|
||||||
(createjsNode.children || []).some(hasAnimations)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendStatus() {
|
|
||||||
if (loadingStatus === "loading") {
|
|
||||||
sendMessage({ type: "status", status: "loading" });
|
|
||||||
} else if (loadingStatus === "loaded") {
|
|
||||||
sendMessage({
|
|
||||||
type: "status",
|
|
||||||
status: "loaded",
|
|
||||||
hasAnimations: hasAnimations(movieClip),
|
|
||||||
});
|
|
||||||
} else if (loadingStatus === "error") {
|
|
||||||
sendMessage({ type: "status", status: "error" });
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`unexpected loadingStatus ${JSON.stringify(loadingStatus)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage(message) {
|
|
||||||
parent.postMessage(message, document.location.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
updateCanvasDimensions();
|
|
||||||
|
|
||||||
// Redraw the stage with the new dimensions - but with `tickOnUpdate` set
|
|
||||||
// to `false`, so that we don't advance by a frame. This keeps us
|
|
||||||
// really-paused if we're paused, and avoids skipping ahead by a frame if
|
|
||||||
// we're playing.
|
|
||||||
stage.tickOnUpdate = false;
|
|
||||||
updateStage();
|
|
||||||
stage.tickOnUpdate = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("message", ({ data }) => {
|
|
||||||
// NOTE: For more sensitive messages, it's important for security to also
|
|
||||||
// check the `origin` property of the incoming event. But in this case, I'm
|
|
||||||
// okay with whatever site is embedding us being able to send play/pause!
|
|
||||||
if (data.type === "play") {
|
|
||||||
play();
|
|
||||||
} else if (data.type === "pause") {
|
|
||||||
pause();
|
|
||||||
} else if (data.type === "requestStatus") {
|
|
||||||
sendStatus();
|
|
||||||
} else {
|
|
||||||
throw new Error(`unexpected message: ${JSON.stringify(data)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
startMovie()
|
|
||||||
.then(() => {
|
|
||||||
sendStatus();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(logPrefix, error);
|
|
||||||
|
|
||||||
loadingStatus = "error";
|
|
||||||
sendStatus();
|
|
||||||
|
|
||||||
// If loading the movie fails, show the fallback image instead, by moving
|
|
||||||
// it out of the canvas content and into the body.
|
|
||||||
document.body.appendChild(document.getElementById("fallback"));
|
|
||||||
console.warn("Showing fallback image instead.");
|
|
||||||
});
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
@import "partials/icon"
|
|
||||||
@import "partials/clean/constants"
|
|
||||||
@import "partials/clean/mixins"
|
|
||||||
|
|
||||||
/* Reset
|
|
||||||
|
|
||||||
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p,
|
|
||||||
blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em,
|
|
||||||
font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b,
|
|
||||||
u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table,
|
|
||||||
caption, tbody, tfoot, thead, tr, th, td
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
border: 0
|
|
||||||
font-size: 100%
|
|
||||||
vertical-align: baseline
|
|
||||||
background: transparent
|
|
||||||
|
|
||||||
/* Typography
|
|
||||||
|
|
||||||
html, body
|
|
||||||
height: 100%
|
|
||||||
|
|
||||||
body
|
|
||||||
background: $background-color
|
|
||||||
color: $text-color
|
|
||||||
font:
|
|
||||||
family: $main-font
|
|
||||||
size: 90%
|
|
||||||
line-height: 1.5
|
|
||||||
|
|
||||||
a[href]
|
|
||||||
color: $link-color
|
|
||||||
|
|
||||||
p
|
|
||||||
font-family: $text-font
|
|
||||||
|
|
||||||
input, button, select
|
|
||||||
font:
|
|
||||||
family: inherit
|
|
||||||
size: 100%
|
|
||||||
|
|
||||||
p
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
h1, h2, h3
|
|
||||||
+header-text
|
|
||||||
|
|
||||||
h1
|
|
||||||
font-size: 3em
|
|
||||||
line-height: 1
|
|
||||||
margin-bottom: 0.50em
|
|
||||||
|
|
||||||
h2
|
|
||||||
font-size: 2em
|
|
||||||
margin-bottom: 0.75em
|
|
||||||
|
|
||||||
h3
|
|
||||||
font-size: 1.5em
|
|
||||||
line-height: 1
|
|
||||||
margin-bottom: 1.00em
|
|
||||||
|
|
||||||
.inline-image
|
|
||||||
margin-right: 1em
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
/* Main
|
|
||||||
|
|
||||||
$container_width: 800px
|
|
||||||
|
|
||||||
#container
|
|
||||||
margin: .25em auto
|
|
||||||
padding-top: $container-top-padding
|
|
||||||
position: relative
|
|
||||||
width: $container_width
|
|
||||||
|
|
||||||
input, button, select, label
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], select, textarea
|
|
||||||
border-radius: 3px
|
|
||||||
background: #fff
|
|
||||||
border: 1px solid $input-border-color
|
|
||||||
color: $text-color + #444444
|
|
||||||
padding: .25em
|
|
||||||
&:focus, &:active
|
|
||||||
color: inherit
|
|
||||||
|
|
||||||
textarea
|
|
||||||
font: inherit
|
|
||||||
|
|
||||||
// TODO: This conflicts with button styles in embedded wardrobe-2020
|
|
||||||
// components. It'd be nice to not apply it to ALL button elements.
|
|
||||||
a.button, input[type=submit], button
|
|
||||||
+awesome-button
|
|
||||||
&.loud
|
|
||||||
+loud-awesome-button
|
|
||||||
.icon
|
|
||||||
margin-right: .25em
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
ul.buttons
|
|
||||||
margin-bottom: 1em
|
|
||||||
li
|
|
||||||
list-style: none
|
|
||||||
margin: 0 .5em
|
|
||||||
&, form
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
#footer
|
|
||||||
clear: both
|
|
||||||
font-size: 75%
|
|
||||||
margin-bottom: 1em
|
|
||||||
padding-top: 2em
|
|
||||||
text-align: center
|
|
||||||
ul, div
|
|
||||||
display: inline
|
|
||||||
margin: 0 1em
|
|
||||||
li, div ul
|
|
||||||
display: inline
|
|
||||||
margin: 0 .5em
|
|
||||||
#locale-form
|
|
||||||
float: right
|
|
||||||
.terms
|
|
||||||
&[data-updated-recently]
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
=flash
|
|
||||||
margin-bottom: 1em
|
|
||||||
padding: .25em .5em
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.notice, .alert, .warning
|
|
||||||
+flash
|
|
||||||
|
|
||||||
.notice
|
|
||||||
+notice
|
|
||||||
|
|
||||||
.alert
|
|
||||||
+error
|
|
||||||
|
|
||||||
.warning
|
|
||||||
+warning
|
|
||||||
|
|
||||||
#userbar
|
|
||||||
+header-text
|
|
||||||
position: absolute
|
|
||||||
right: 0
|
|
||||||
top: 0
|
|
||||||
> *
|
|
||||||
display: inline
|
|
||||||
margin: 0 .25em
|
|
||||||
|
|
||||||
#userbar-image-mode
|
|
||||||
font-weight: bold
|
|
||||||
margin-right: 1em
|
|
||||||
text-decoration: none
|
|
||||||
img
|
|
||||||
+icon
|
|
||||||
|
|
||||||
#userbar-log-in
|
|
||||||
text-decoration: none
|
|
||||||
img
|
|
||||||
margin:
|
|
||||||
bottom: -4px
|
|
||||||
right: .25em
|
|
||||||
span
|
|
||||||
text-decoration: underline
|
|
||||||
&:hover span
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
.object
|
|
||||||
+inline-block
|
|
||||||
margin: $object-padding 0
|
|
||||||
padding: 0 $object-padding
|
|
||||||
position: relative
|
|
||||||
text-align: center
|
|
||||||
vertical-align: top
|
|
||||||
width: $object-width
|
|
||||||
a
|
|
||||||
text-decoration: none
|
|
||||||
img
|
|
||||||
+opacity(0.75)
|
|
||||||
img
|
|
||||||
display: block
|
|
||||||
height: $object-img-size
|
|
||||||
margin: 0 auto
|
|
||||||
width: $object-img-size
|
|
||||||
&:hover img, a:hover img
|
|
||||||
// behave in browsers that only respond to a:hover, but also be in the
|
|
||||||
// hover state more often for browsers who support div:hover
|
|
||||||
// (quantity form in user items)
|
|
||||||
+opacity(1)
|
|
||||||
|
|
||||||
.nc-icon, .closeted-icons
|
|
||||||
+opacity(1)
|
|
||||||
background: rgba(255, 255, 255, 0.75)
|
|
||||||
line-height: 1
|
|
||||||
position: absolute
|
|
||||||
top: $object-img-size - $nc-icon-size
|
|
||||||
&:hover
|
|
||||||
+opacity(0.5)
|
|
||||||
background: transparent
|
|
||||||
|
|
||||||
.nc-icon, .closeted-icons img
|
|
||||||
display: inline
|
|
||||||
height: $nc-icon-size
|
|
||||||
width: $nc-icon-size
|
|
||||||
|
|
||||||
.nc-icon
|
|
||||||
right: ($object-width - $object-img-size) / 2 + $object-padding
|
|
||||||
|
|
||||||
$closeted-icons-left: ($object-width - $object-img-size) / 2 + $object-padding
|
|
||||||
.closeted-icons
|
|
||||||
left: $closeted-icons-left
|
|
||||||
|
|
||||||
dt
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
dd
|
|
||||||
margin: 0 0 1.5em 1em
|
|
||||||
|
|
||||||
#home-link
|
|
||||||
+header-text
|
|
||||||
font:
|
|
||||||
size: 175%
|
|
||||||
weight: bold
|
|
||||||
left: 0
|
|
||||||
line-height: 1
|
|
||||||
padding-left: .25em
|
|
||||||
padding-right: .25em
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
&:hover
|
|
||||||
background: $module-bg-color
|
|
||||||
text-decoration: none
|
|
||||||
span:before
|
|
||||||
content: "<< "
|
|
||||||
|
|
||||||
#home-link, #userbar
|
|
||||||
padding-top: 6px
|
|
||||||
padding-bottom: 6px
|
|
||||||
|
|
||||||
.pagination
|
|
||||||
a, span
|
|
||||||
margin: 0 .5em
|
|
||||||
.current
|
|
||||||
font-weight: bold
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
body.use-responsive-design
|
|
||||||
#container
|
|
||||||
max-width: 100%
|
|
||||||
padding-inline: 1rem
|
|
||||||
box-sizing: border-box
|
|
||||||
|
|
||||||
#home-link
|
|
||||||
margin-left: 1rem
|
|
||||||
padding-inline: 0
|
|
||||||
|
|
||||||
#userbar
|
|
||||||
margin-right: 1rem
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
body.alt_styles-index
|
|
||||||
.alt-styles-header
|
|
||||||
margin-top: 1em
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.alt-styles-list
|
|
||||||
list-style: none
|
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
||||||
gap: 1.5em
|
|
||||||
|
|
||||||
.alt-style
|
|
||||||
text-align: center
|
|
||||||
width: 80px
|
|
||||||
|
|
||||||
.alt-style-thumbnail
|
|
||||||
width: 80px
|
|
||||||
height: 80px
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
@charset "UTF-8"
|
|
||||||
|
|
||||||
@import partials/clean/constants
|
|
||||||
@import partials/clean/mixins
|
|
||||||
|
|
||||||
@import layout
|
|
||||||
@import responsive
|
|
||||||
|
|
||||||
@import partials/jquery.jgrowl
|
|
||||||
|
|
||||||
@import alt_styles/index
|
|
||||||
@import closet_hangers/index
|
|
||||||
@import closet_hangers/petpage
|
|
||||||
@import closet_lists/form
|
|
||||||
@import neopets_page_import_tasks/new
|
|
||||||
@import contributions/index
|
|
||||||
@import outfits/index
|
|
||||||
@import outfits/new
|
|
||||||
@import pets/bulk
|
|
||||||
@import users/top_contributors
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
.hanger-spinner {
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
animation: 1.2s infinite hanger-spinner-swing;
|
|
||||||
transform-origin: top center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
animation: 1.6s infinite hanger-spinner-fade-pulse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Adapted from animate.css "swing". We spend 75% of the time swinging,
|
|
||||||
then 25% of the time pausing before the next loop.
|
|
||||||
|
|
||||||
We use this animation for folks who are okay with dizzy-ish motion.
|
|
||||||
For reduced motion, we use a pulse-fade instead.
|
|
||||||
*/
|
|
||||||
@keyframes hanger-spinner-swing {
|
|
||||||
15% {
|
|
||||||
transform: rotate3d(0, 0, 1, 15deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
transform: rotate3d(0, 0, 1, -10deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
45% {
|
|
||||||
transform: rotate3d(0, 0, 1, 5deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
60% {
|
|
||||||
transform: rotate3d(0, 0, 1, -5deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
transform: rotate3d(0, 0, 1, 0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate3d(0, 0, 1, 0deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
A homebrew fade-pulse animation. We use this for folks who don't
|
|
||||||
like motion. It's an important accessibility thing!
|
|
||||||
*/
|
|
||||||
@keyframes hanger-spinner-fade-pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
@import "../partials/clean/constants"
|
|
||||||
|
|
||||||
.settings-form
|
|
||||||
border: 1px solid $module-border-color
|
|
||||||
background: $module-bg-color
|
|
||||||
border-radius: 1em
|
|
||||||
padding: 1em 1.25em
|
|
||||||
|
|
||||||
&:not(:last-of-type)
|
|
||||||
margin-bottom: 2em
|
|
||||||
|
|
||||||
h2
|
|
||||||
font-size: 1.5rem
|
|
||||||
margin-bottom: .25em
|
|
||||||
|
|
||||||
.hint
|
|
||||||
font-style: italic
|
|
||||||
font-size: .85em
|
|
||||||
opacity: .9
|
|
||||||
|
|
||||||
fieldset
|
|
||||||
padding-block: .5em
|
|
||||||
|
|
||||||
fieldset:not(:last-of-type)
|
|
||||||
border-bottom: 1px solid $module-border-color
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.field
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
.field_with_errors
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
label
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.error-explanation
|
|
||||||
color: $error-color
|
|
||||||
background: $error-bg-color
|
|
||||||
border: 1px solid $error-border-color
|
|
||||||
border-radius: .5em
|
|
||||||
padding: .5em
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
header
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
ul
|
|
||||||
padding-left: 2em
|
|
||||||
|
|
||||||
.neopass-info
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.neopass-explanation
|
|
||||||
font-size: .85em
|
|
||||||
|
|
@ -1,470 +0,0 @@
|
||||||
@import "../partials/clean/constants"
|
|
||||||
@import "../partials/campaign-progress"
|
|
||||||
@import "../partials/context_button"
|
|
||||||
@import "../partials/icon"
|
|
||||||
@import "../partials/secondary_nav"
|
|
||||||
|
|
||||||
body.closet_hangers-index
|
|
||||||
+campaign-progress
|
|
||||||
+secondary-nav
|
|
||||||
|
|
||||||
#title
|
|
||||||
margin-bottom: 0
|
|
||||||
|
|
||||||
#import-link
|
|
||||||
+awesome-button
|
|
||||||
+loud-awesome-button-color
|
|
||||||
|
|
||||||
#closet-hangers-items-search
|
|
||||||
float: right
|
|
||||||
|
|
||||||
input[name=q]
|
|
||||||
&.loading
|
|
||||||
background:
|
|
||||||
image: image-url("loading.gif")
|
|
||||||
position: 2px center
|
|
||||||
repeat: no-repeat
|
|
||||||
padding-left: $icon-width + 4px
|
|
||||||
|
|
||||||
#closet-hangers-contact
|
|
||||||
clear: both
|
|
||||||
color: $soft-text-color
|
|
||||||
margin-bottom: 1em
|
|
||||||
margin-left: 2em
|
|
||||||
min-height: image-height("neomail.png")
|
|
||||||
|
|
||||||
a
|
|
||||||
color: inherit
|
|
||||||
margin-right: .5em
|
|
||||||
text-decoration: none
|
|
||||||
&:hover
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
a, > form
|
|
||||||
background:
|
|
||||||
position: left center
|
|
||||||
repeat: no-repeat
|
|
||||||
padding-left: image-width("neomail.png") + 4px
|
|
||||||
|
|
||||||
a.neomail, > form
|
|
||||||
background-image: image-url("neomail.png")
|
|
||||||
|
|
||||||
a.lookup
|
|
||||||
background-image: image-url("lookup.png")
|
|
||||||
|
|
||||||
select
|
|
||||||
width: 10em
|
|
||||||
|
|
||||||
label
|
|
||||||
font-weight: bold
|
|
||||||
margin-right: .5em
|
|
||||||
|
|
||||||
&:after
|
|
||||||
content: ":"
|
|
||||||
#toggle-help, #toggle-compare
|
|
||||||
+awesome-button
|
|
||||||
cursor: pointer
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#closet-hangers-help.hidden
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#closet-hangers-extras
|
|
||||||
font-size: 85%
|
|
||||||
margin:
|
|
||||||
bottom: 2em
|
|
||||||
top: 2em
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
a
|
|
||||||
+awesome-button
|
|
||||||
margin: 0 0.5em
|
|
||||||
|
|
||||||
#closet-hangers-share
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
label
|
|
||||||
font-weight: bold
|
|
||||||
margin-right: .5em
|
|
||||||
|
|
||||||
input
|
|
||||||
width: 30em
|
|
||||||
|
|
||||||
.bulk-actions
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#closet-hangers
|
|
||||||
clear: both
|
|
||||||
text-align: center
|
|
||||||
border-top: 1px solid $module-border-color
|
|
||||||
|
|
||||||
.object
|
|
||||||
.quantity
|
|
||||||
+opacity(.75)
|
|
||||||
background: white
|
|
||||||
padding: 6px 4px 4px
|
|
||||||
position: absolute
|
|
||||||
left: ($object-width - $object-img-size) / 2 + $object-padding
|
|
||||||
line-height: 1
|
|
||||||
text-align: left
|
|
||||||
top: 0
|
|
||||||
|
|
||||||
span, input[type=number]
|
|
||||||
font-size: 16px
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
form
|
|
||||||
display: none
|
|
||||||
|
|
||||||
&[data-quantity="1"]
|
|
||||||
.quantity
|
|
||||||
display: none
|
|
||||||
|
|
||||||
a
|
|
||||||
/* It's jarring to have line-height gaps in the linkiness. */
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
.object
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
label
|
|
||||||
display: block
|
|
||||||
|
|
||||||
input[type=checkbox]
|
|
||||||
display: none
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
right: ($object-width - $object-img-size) / 2 + $object-padding
|
|
||||||
height: 16px
|
|
||||||
width: 16px
|
|
||||||
|
|
||||||
&:checked
|
|
||||||
display: block
|
|
||||||
|
|
||||||
& + label
|
|
||||||
background: $module-bg-color
|
|
||||||
outline: 1px solid $module-border-color
|
|
||||||
|
|
||||||
.closet-hangers-group
|
|
||||||
border-top: 1px solid $module-border-color
|
|
||||||
margin-bottom: 2em
|
|
||||||
padding-bottom: 1em
|
|
||||||
|
|
||||||
&:first-of-type
|
|
||||||
border-top: 0
|
|
||||||
|
|
||||||
> header
|
|
||||||
border-bottom: 1px solid $soft-border-color
|
|
||||||
display: block
|
|
||||||
margin-bottom: .25em
|
|
||||||
padding: .25em 0
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
h3
|
|
||||||
font-size: 250%
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
.add-closet-list
|
|
||||||
+awesome-button
|
|
||||||
bottom: 50%
|
|
||||||
margin-bottom: -1em
|
|
||||||
position: absolute
|
|
||||||
right: 1em
|
|
||||||
|
|
||||||
&:active
|
|
||||||
margin-bottom: -1.1em
|
|
||||||
top: auto
|
|
||||||
|
|
||||||
span.show, span.hide
|
|
||||||
color: $soft-text-color
|
|
||||||
display: none
|
|
||||||
font-size: 85%
|
|
||||||
left: 1em
|
|
||||||
position: absolute
|
|
||||||
top: 1em
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: inherit
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
.closet-list
|
|
||||||
border-bottom: 1px solid $soft-border-color
|
|
||||||
padding: .5em 0
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
.visibility-form
|
|
||||||
font-size: 85%
|
|
||||||
left: .5em
|
|
||||||
position: absolute
|
|
||||||
text-align: left
|
|
||||||
top: .25em
|
|
||||||
z-index: 10
|
|
||||||
|
|
||||||
input, select
|
|
||||||
font-size: inherit
|
|
||||||
margin:
|
|
||||||
bottom: 0
|
|
||||||
top: 0
|
|
||||||
|
|
||||||
select
|
|
||||||
border-color: $background-color
|
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
+context-button
|
|
||||||
font-size: inherit
|
|
||||||
visibility: hidden
|
|
||||||
|
|
||||||
&:active
|
|
||||||
top: 1px
|
|
||||||
|
|
||||||
.visibility-descriptions
|
|
||||||
+opacity(.75)
|
|
||||||
background: $background-color
|
|
||||||
font-style: italic
|
|
||||||
list-style: none
|
|
||||||
padding: 0 .5em
|
|
||||||
|
|
||||||
li
|
|
||||||
display: none
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
.visibility-descriptions li.current
|
|
||||||
display: block
|
|
||||||
|
|
||||||
header
|
|
||||||
display: block
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
h4
|
|
||||||
+header-text
|
|
||||||
font-size: 150%
|
|
||||||
line-height: 1
|
|
||||||
margin: 0 auto .67em
|
|
||||||
width: 50%
|
|
||||||
|
|
||||||
.empty-list
|
|
||||||
display: none
|
|
||||||
font-style: italic
|
|
||||||
|
|
||||||
.closet-list-controls
|
|
||||||
display: none
|
|
||||||
position: absolute
|
|
||||||
right: 1em
|
|
||||||
top: 0
|
|
||||||
|
|
||||||
a, input[type=submit], button
|
|
||||||
+context-button
|
|
||||||
|
|
||||||
form
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
&[data-hangers-count="0"]
|
|
||||||
.empty-list
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&.unlisted
|
|
||||||
h4
|
|
||||||
font:
|
|
||||||
size: 125%
|
|
||||||
style: italic
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
.closet-list-controls
|
|
||||||
display: block
|
|
||||||
|
|
||||||
.visibility-form
|
|
||||||
input[type=submit]
|
|
||||||
visibility: visible
|
|
||||||
|
|
||||||
select
|
|
||||||
border-color: $soft-border-color
|
|
||||||
|
|
||||||
&:last-child
|
|
||||||
border-bottom: 0
|
|
||||||
|
|
||||||
&.droppable-active
|
|
||||||
border-radius: 1em
|
|
||||||
+module
|
|
||||||
border-bottom-width: 1px
|
|
||||||
border-style: dotted
|
|
||||||
margin: 1em 0
|
|
||||||
|
|
||||||
.object
|
|
||||||
// totally hiding these elements causes the original element to change
|
|
||||||
// position, throwing off the drag
|
|
||||||
+opacity(.25)
|
|
||||||
&.ui-draggable-dragging
|
|
||||||
+opacity(1)
|
|
||||||
|
|
||||||
.closet-list-controls
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.closet-list-hangers
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.visibility-form
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.closet-hangers-group-autocomplete-item, .closet-list-autocomplete-item
|
|
||||||
span
|
|
||||||
+opacity(.5)
|
|
||||||
font-style: italic
|
|
||||||
padding: .2em .4em
|
|
||||||
|
|
||||||
.closet-list-autocomplete-item
|
|
||||||
a, span
|
|
||||||
font-size: 85%
|
|
||||||
padding-left: 2em
|
|
||||||
|
|
||||||
.closet-hangers-group
|
|
||||||
&[data-owned=true] .user-wants, &[data-owned=false] .user-owns
|
|
||||||
background: $module-bg-color
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
&.current-user
|
|
||||||
#closet-hangers
|
|
||||||
.object:hover
|
|
||||||
form
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
.closet-hanger-destroy
|
|
||||||
position: absolute
|
|
||||||
right: ($object-width - $object-img-size) / 2 + $object-padding
|
|
||||||
top: $object-img-size - 28px
|
|
||||||
|
|
||||||
input
|
|
||||||
+context-button
|
|
||||||
|
|
||||||
.quantity
|
|
||||||
+opacity(1)
|
|
||||||
background: transparent
|
|
||||||
top: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
span
|
|
||||||
display: none
|
|
||||||
|
|
||||||
input[type=number]
|
|
||||||
padding: 2px
|
|
||||||
width: 2em
|
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
font-size: 85%
|
|
||||||
|
|
||||||
input[type=checkbox]
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&.js
|
|
||||||
#closet-hangers
|
|
||||||
.object:hover .quantity
|
|
||||||
display: block
|
|
||||||
|
|
||||||
input[type=number]
|
|
||||||
width: 2.5em
|
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.object.loading
|
|
||||||
background: $module-bg-color
|
|
||||||
outline: 1px solid $module-border-color
|
|
||||||
|
|
||||||
.quantity
|
|
||||||
display: block
|
|
||||||
|
|
||||||
span:after
|
|
||||||
content: "…"
|
|
||||||
|
|
||||||
.bulk-actions
|
|
||||||
background: $background-color
|
|
||||||
box-sizing: border-box
|
|
||||||
display: block
|
|
||||||
font-size: 85%
|
|
||||||
padding: .5em 1em
|
|
||||||
text-align: center
|
|
||||||
width: 800px
|
|
||||||
|
|
||||||
position: sticky
|
|
||||||
top: 0
|
|
||||||
border-bottom: 1px solid $module-border-color
|
|
||||||
z-index: 11 /* beat the visibility form */
|
|
||||||
|
|
||||||
.bulk-actions-intro, .bulk-actions-target-desc-singular
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.bulk-actions-target-desc
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
.bulk-actions-options
|
|
||||||
display: inline-block
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
> li
|
|
||||||
display: inline-block
|
|
||||||
margin-left: .75em
|
|
||||||
|
|
||||||
form
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
&:not(:first-child)::before
|
|
||||||
content: " or "
|
|
||||||
display: inline-block
|
|
||||||
margin-right: .75em
|
|
||||||
|
|
||||||
&[data-target-count="0"]
|
|
||||||
.bulk-actions-intro
|
|
||||||
display: block
|
|
||||||
|
|
||||||
.bulk-actions-form
|
|
||||||
display: none
|
|
||||||
|
|
||||||
&[data-target-count="1"]
|
|
||||||
.bulk-actions-target-desc-singular
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
.bulk-actions-target-desc-plural
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#closet-hangers-contact
|
|
||||||
input[type=submit]
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.closet-hangers-group
|
|
||||||
header
|
|
||||||
.show, .hide
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.hide
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&.hidden
|
|
||||||
header .hide, .closet-hangers-group-content
|
|
||||||
display: none
|
|
||||||
|
|
||||||
header .show
|
|
||||||
display: block
|
|
||||||
|
|
||||||
#toggle-help
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
.remove-all
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.select-all
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
&.js
|
|
||||||
#toggle-compare
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
#closet-hangers.comparing
|
|
||||||
.object
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.closet-hangers-group
|
|
||||||
&[data-owned=true] .user-wants, &[data-owned=false] .user-owns
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
@import "../partials/clean/constants"
|
|
||||||
@import "../partials/clean/mixins"
|
|
||||||
@import "../partials/secondary_nav"
|
|
||||||
|
|
||||||
body.closet_hangers-petpage
|
|
||||||
+secondary-nav
|
|
||||||
|
|
||||||
#intro
|
|
||||||
clear: both
|
|
||||||
|
|
||||||
#petpage-closet-lists
|
|
||||||
+clearfix
|
|
||||||
border-radius: 10px
|
|
||||||
border: 1px solid $soft-border-color
|
|
||||||
margin-bottom: 1.5em
|
|
||||||
padding: .5em 1.5em
|
|
||||||
|
|
||||||
> div
|
|
||||||
margin: .25em 0
|
|
||||||
|
|
||||||
h4
|
|
||||||
display: inline-block
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
&::after
|
|
||||||
content: ":"
|
|
||||||
|
|
||||||
ul
|
|
||||||
list-style: none
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline-block
|
|
||||||
font-size: 85%
|
|
||||||
margin: .25em .5em
|
|
||||||
padding: 1px
|
|
||||||
|
|
||||||
label
|
|
||||||
padding: .25em .75em .25em .25em
|
|
||||||
|
|
||||||
&.checked
|
|
||||||
background: $module-bg-color
|
|
||||||
border-radius: 3px
|
|
||||||
border: 1px solid $module-border-color
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
&.unlisted
|
|
||||||
font-style: italic
|
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
float: right
|
|
||||||
|
|
||||||
#petpage-output
|
|
||||||
display: block
|
|
||||||
height: 30em
|
|
||||||
margin: 0 auto
|
|
||||||
width: 50%
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
@import "../partials/secondary_nav"
|
|
||||||
@import "../partials/clean/mixins"
|
|
||||||
|
|
||||||
body.closet_lists-new, body.closet_lists-create, body.closet_lists-edit, body.closet_lists-update
|
|
||||||
+secondary-nav
|
|
||||||
|
|
||||||
form ul.fields
|
|
||||||
clear: both
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
label
|
|
||||||
float: left
|
|
||||||
font-weight: bold
|
|
||||||
margin-right: 1em
|
|
||||||
|
|
||||||
li
|
|
||||||
padding: 0.75em 0
|
|
||||||
width: 35em
|
|
||||||
|
|
||||||
input, textarea, select
|
|
||||||
clear: both
|
|
||||||
display: block
|
|
||||||
margin-top: .25em
|
|
||||||
width: 80%
|
|
||||||
|
|
||||||
textarea
|
|
||||||
height: 12em
|
|
||||||
|
|
||||||
.hint
|
|
||||||
display: block
|
|
||||||
font:
|
|
||||||
size: 85%
|
|
||||||
|
|
||||||
.trade-warning
|
|
||||||
+warning
|
|
||||||
margin-bottom: 1em
|
|
||||||
padding: .75em .5em
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
p:last-of-type
|
|
||||||
margin-bottom: 0
|
|
||||||
|
|
||||||
// Only show the trade warning when the list is marked as Trading!
|
|
||||||
form:not([data-list-visibility="2"]) .trade-warning
|
|
||||||
display: none
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
@import "../partials/clean/mixins"
|
|
||||||
|
|
||||||
body.contributions-index
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.contributions
|
|
||||||
li
|
|
||||||
list-style: none
|
|
||||||
height: 80px
|
|
||||||
overflow: hidden
|
|
||||||
padding: 1em 0 0 100px
|
|
||||||
position: relative
|
|
||||||
text-align: left
|
|
||||||
.point-value
|
|
||||||
+header-text
|
|
||||||
color: #fff
|
|
||||||
font-size: 80px
|
|
||||||
left: 0
|
|
||||||
line-height: 1
|
|
||||||
position: absolute
|
|
||||||
text-align: center
|
|
||||||
text-shadow: 2px 2px 0 #000
|
|
||||||
top: 0
|
|
||||||
width: 80px
|
|
||||||
z-index: 3
|
|
||||||
&:hover
|
|
||||||
+opacity(0.5)
|
|
||||||
img
|
|
||||||
height: 80px
|
|
||||||
left: 0
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
width: 80px
|
|
||||||
z-index: 2
|
|
||||||
.username, .contributed-name
|
|
||||||
font-weight: bold
|
|
||||||
.time-ago
|
|
||||||
display: block
|
|
||||||
font-size: 75%
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
@import "../../partials/clean/constants"
|
|
||||||
|
|
||||||
#title
|
|
||||||
text-align: center
|
|
||||||
font-size: 2.5rem
|
|
||||||
|
|
||||||
.login-options
|
|
||||||
display: flex
|
|
||||||
margin-block: 3em
|
|
||||||
|
|
||||||
section
|
|
||||||
flex: 1 1 0
|
|
||||||
padding-block: 1em
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
justify-content: center
|
|
||||||
align-items: center
|
|
||||||
|
|
||||||
&:not(:last-of-type)
|
|
||||||
border-right: 1px solid $module-border-color
|
|
||||||
margin-right: 1em
|
|
||||||
padding-right: 1em
|
|
||||||
|
|
||||||
.login-option-neopass
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
header
|
|
||||||
font-weight: bold
|
|
||||||
font-size: 125%
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.neopass-explanation
|
|
||||||
font-size: 85%
|
|
||||||
|
|
||||||
summary
|
|
||||||
cursor: pointer
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
.login-form
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
.field
|
|
||||||
margin-bottom: .75em
|
|
||||||
|
|
||||||
.input-field label
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.actions
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: .5em
|
|
||||||
|
|
||||||
.remember-me
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: .25em
|
|
||||||
font-size: 85%
|
|
||||||
|
|
||||||
input[type=checkbox]
|
|
||||||
height: 1em
|
|
||||||
width: 1em
|
|
||||||
|
|
||||||
.login-links
|
|
||||||
font-size: 85%
|
|
||||||
display: flex
|
|
||||||
gap: .5em
|
|
||||||
|
|
||||||
.log-in-with-neopass-button
|
|
||||||
background: linear-gradient(#ebb233, #f6e250, #ebb233)
|
|
||||||
color: #111
|
|
||||||
font-size: 1rem
|
|
||||||
text-shadow: none
|
|
||||||
font-family: "Arial Black", sans-serif
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: #111 // override default DTI styles
|
|
||||||
filter: brightness(105%)
|
|
||||||
|
|
||||||
.neopass-icon
|
|
||||||
vertical-align: middle
|
|
||||||
height: 2em
|
|
||||||
width: 2em
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
/* A font by Jos Buivenga (exljbris) -> www.exljbris.nl */
|
|
||||||
@font-face {
|
|
||||||
font-family: Delicious;
|
|
||||||
src: local("Delicious"), url("<%= font_path "Delicious-Roman.otf" %>)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Delicious;
|
|
||||||
font-weight: bold;
|
|
||||||
src: local("Delicious"), url("<%= font_path "Delicious-Bold.otf" %>");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Delicious;
|
|
||||||
font-style: italic;
|
|
||||||
src: local("Delicious"), url("<%= font_path "Delicious-Italic.otf" %>");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Noto Sans";
|
|
||||||
src: local("Noto Sans"), url("<%= font_path "NotoSans-Variable.ttf" %>");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Noto Sans";
|
|
||||||
font-style: italic;
|
|
||||||
src: local("Noto Sans"), url("<%= font_path "NotoSans-Italic-Variable.ttf" %>");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Noto Serif";
|
|
||||||
src: local("Noto Serif"), url("<%= font_path "NotoSerif-Variable.ttf" %>");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Noto Serif";
|
|
||||||
font-style: italic;
|
|
||||||
src: local("Noto Serif"), url("<%= font_path "NotoSerif-Italic-Variable.ttf" %>");
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
@import "../../partials/clean/constants"
|
|
||||||
@import "../../partials/campaign-progress"
|
|
||||||
@import "../../partials/outfit"
|
|
||||||
|
|
||||||
/* TODO: redundant with outfits/index; why is it not in the partial? */
|
|
||||||
$outfit-inner-height: 150px
|
|
||||||
$outfit-inner-width: 150px
|
|
||||||
$outfit-banner-h-padding: 4px
|
|
||||||
$outfit-banner-v-padding: 2px
|
|
||||||
$outfit-banner-inner-width: $outfit-inner-width - (2 * $outfit-banner-h-padding)
|
|
||||||
|
|
||||||
body
|
|
||||||
+campaign-progress
|
|
||||||
color: $campaign-text-color
|
|
||||||
|
|
||||||
a
|
|
||||||
color: $campaign-text-color + #222 !important
|
|
||||||
|
|
||||||
#home-link:hover
|
|
||||||
background-color: $campaign-background-color
|
|
||||||
|
|
||||||
#userbar, #footer
|
|
||||||
color: $text-color
|
|
||||||
a
|
|
||||||
color: $link-color
|
|
||||||
|
|
||||||
#home-link
|
|
||||||
color: $link-color
|
|
||||||
|
|
||||||
#title
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#donation-form
|
|
||||||
+module
|
|
||||||
background: $campaign-background-color
|
|
||||||
border-color: $campaign-border-color
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
margin-top: 1em
|
|
||||||
margin-bottom: 1.5em
|
|
||||||
padding-bottom: 32px
|
|
||||||
padding-left: 24px
|
|
||||||
padding-right: 24px
|
|
||||||
padding-top: 32px
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
// We ignore the theme attribute on campaigns now, and just do purple.
|
|
||||||
&::after
|
|
||||||
background:
|
|
||||||
image: url(image_path("fundraising/purple.png"))
|
|
||||||
repeat: no-repeat
|
|
||||||
bottom: 0
|
|
||||||
content: " "
|
|
||||||
height: 123px
|
|
||||||
position: absolute
|
|
||||||
right: 4px
|
|
||||||
width: 150px
|
|
||||||
|
|
||||||
header, #donation-fields
|
|
||||||
flex: 1
|
|
||||||
|
|
||||||
#donation-form-title
|
|
||||||
font-size: 125%
|
|
||||||
font-weight: bold
|
|
||||||
margin-bottom: .25em
|
|
||||||
margin-top: 0
|
|
||||||
|
|
||||||
p
|
|
||||||
font-family: $main-font
|
|
||||||
font-size: 85%
|
|
||||||
margin-bottom: 0
|
|
||||||
margin-top: .5em
|
|
||||||
|
|
||||||
#donation-fields
|
|
||||||
margin-left: 20px
|
|
||||||
padding-top: 7px
|
|
||||||
|
|
||||||
#amount-header
|
|
||||||
font-size: 85%
|
|
||||||
font-weight: bold
|
|
||||||
line-height: 1
|
|
||||||
|
|
||||||
#amount-choices
|
|
||||||
$amount-choices-border-color: desaturate(lighten($campaign-border-color, 30%), 30%)
|
|
||||||
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
margin-bottom: .75em
|
|
||||||
margin-top: .5em
|
|
||||||
|
|
||||||
li
|
|
||||||
border: 1px solid $amount-choices-border-color
|
|
||||||
border-radius: 5px
|
|
||||||
display: block
|
|
||||||
flex: 1
|
|
||||||
list-style: none
|
|
||||||
overflow: hidden
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
&:not(:last-of-type)
|
|
||||||
margin-right: .75em
|
|
||||||
|
|
||||||
input[type=radio]
|
|
||||||
height: 0
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
opacity: 0
|
|
||||||
position: absolute
|
|
||||||
width: 0
|
|
||||||
|
|
||||||
label
|
|
||||||
border: 1px solid transparent
|
|
||||||
box-sizing: border-box
|
|
||||||
display: block
|
|
||||||
padding: .5em .5em
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
input[type=radio]:checked ~ label
|
|
||||||
background: lighten($amount-choices-border-color, 15%)
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
input[type=radio]:focus ~ label
|
|
||||||
border-color: white
|
|
||||||
border-radius: 5px
|
|
||||||
|
|
||||||
#amount-custom-fields
|
|
||||||
display: none
|
|
||||||
|
|
||||||
input[type=text]
|
|
||||||
font-family: inherit
|
|
||||||
font-size: inherit
|
|
||||||
line-height: 1
|
|
||||||
padding: 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
#amount-custom:checked ~ #amount-custom-fields
|
|
||||||
display: block
|
|
||||||
|
|
||||||
#amount-custom:checked ~ label[for=amount-custom]
|
|
||||||
display: none
|
|
||||||
|
|
||||||
input[type=text]
|
|
||||||
border-color: #cce
|
|
||||||
color: $campaign-text-color
|
|
||||||
width: 3em
|
|
||||||
|
|
||||||
#donation-controls
|
|
||||||
button
|
|
||||||
+awesome-button-color(#004)
|
|
||||||
font-size: 120%
|
|
||||||
|
|
||||||
#campaign-text[data-campaign-complete]
|
|
||||||
#description
|
|
||||||
display: none
|
|
||||||
&[data-show]
|
|
||||||
display: block
|
|
||||||
|
|
||||||
#success-thanks
|
|
||||||
border: 1px dashed $module-border-color
|
|
||||||
margin-bottom: 1em
|
|
||||||
padding: 1em
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
p:last-child
|
|
||||||
margin-bottom: 0
|
|
||||||
|
|
||||||
#success-thanks-toggle-description
|
|
||||||
position: absolute
|
|
||||||
bottom: 1em
|
|
||||||
font-style: italic
|
|
||||||
right: 1em
|
|
||||||
|
|
||||||
#outfits
|
|
||||||
+outfits-list
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
> li
|
|
||||||
+outfit
|
|
||||||
|
|
||||||
height: $outfit-inner-height
|
|
||||||
margin: 2px
|
|
||||||
width: $outfit-inner-width
|
|
||||||
|
|
||||||
header, footer
|
|
||||||
font-size: 85%
|
|
||||||
padding: $outfit-banner-v-padding $outfit-banner-h-padding
|
|
||||||
width: $outfit-banner-inner-width
|
|
||||||
|
|
||||||
img
|
|
||||||
height: $outfit-inner-height
|
|
||||||
width: $outfit-inner-width
|
|
||||||
|
|
||||||
&.banner
|
|
||||||
background-image: url(https://images.neopets.com/themes/004_bir_a2e60/footer_bg.png)
|
|
||||||
background-position: 0 -60px
|
|
||||||
border: 2px solid #006
|
|
||||||
color: white
|
|
||||||
height: 100px
|
|
||||||
line-height: 100px
|
|
||||||
margin: 4px 0
|
|
||||||
text-shadow: #335 2px 2px 1px
|
|
||||||
width: 800px - 4px
|
|
||||||
|
|
||||||
span
|
|
||||||
+inline-block
|
|
||||||
font-size: 32px
|
|
||||||
font-weight: bold
|
|
||||||
line-height: 1.5
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
#last-years-donors
|
|
||||||
font-weight: bold
|
|
||||||
margin-top: 1em
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
#outfits-header > *
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
#all-campaigns-list
|
|
||||||
li
|
|
||||||
display: inline-block
|
|
||||||
list-style: none
|
|
||||||
margin-left: 1em
|
|
||||||
|
|
||||||
#fine-print
|
|
||||||
font-size: 85%
|
|
||||||
margin-top: 2em
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
@import "../../partials/clean/constants"
|
|
||||||
@import "../../partials/clean/mixins"
|
|
||||||
|
|
||||||
#thank-you
|
|
||||||
border: 3px solid $module-border-color
|
|
||||||
display: block
|
|
||||||
margin: 0 auto 1em
|
|
||||||
|
|
||||||
#edit-donation
|
|
||||||
ul
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
label
|
|
||||||
+inline-block
|
|
||||||
width: 16em
|
|
||||||
|
|
||||||
.name input[type=text]
|
|
||||||
width: 10em
|
|
||||||
|
|
||||||
.feature input[type=text]
|
|
||||||
width: 23em
|
|
||||||
|
|
||||||
.choose-outfit
|
|
||||||
font-size: 85%
|
|
||||||
margin-left: 1em
|
|
||||||
|
|
||||||
select
|
|
||||||
width: 10em
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
@import "../partials/item_header"
|
|
||||||
|
|
||||||
.item-header
|
|
||||||
+item-header
|
|
||||||
|
|
||||||
.item-subpage-title
|
|
||||||
text-align: left
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.trades-table
|
|
||||||
text-align: left
|
|
||||||
width: 100%
|
|
||||||
table-layout: fixed
|
|
||||||
|
|
||||||
th, td
|
|
||||||
&:nth-child(1), &:nth-child(2)
|
|
||||||
width: 15ch
|
|
||||||
overflow: hidden
|
|
||||||
text-overflow: ellipsis
|
|
||||||
|
|
||||||
.trade-list-names
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
&:not(:last-child)::after
|
|
||||||
content: ", "
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
=main_unit
|
|
||||||
float: left
|
|
||||||
width: 49%
|
|
||||||
h2
|
|
||||||
font-size: 125%
|
|
||||||
|
|
||||||
form
|
|
||||||
margin-bottom: 2em
|
|
||||||
|
|
||||||
#search-info
|
|
||||||
+main_unit
|
|
||||||
padding-right: 1%
|
|
||||||
dl
|
|
||||||
text-align: left
|
|
||||||
dd
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
#species-search-links
|
|
||||||
+main_unit
|
|
||||||
padding-left: 1%
|
|
||||||
img
|
|
||||||
height: 80px
|
|
||||||
width: 80px
|
|
||||||