Merge pull request #52 from chibicitiberiu/development
Integrated django_dynamic_preferences and created first time setup wizard
@ -10,12 +10,13 @@ COPY ./requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ENV YTSM_DEBUG='False'
|
||||
ENV YTSM_DATA_PATH='/usr/src/ytsm/data'
|
||||
ENV YTSM_DATA_DIR='/usr/src/ytsm/data'
|
||||
|
||||
VOLUME /usr/src/ytsm/config
|
||||
VOLUME /usr/src/ytsm/data
|
||||
VOLUME /usr/src/ytsm/download
|
||||
|
||||
COPY ./app/ ./
|
||||
COPY ./config/ ./
|
||||
COPY ./docker/init.sh ./
|
||||
|
||||
EXPOSE 8000
|
||||
|
@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from os.path import dirname as up
|
||||
|
||||
@ -23,10 +24,12 @@ SESSION_COOKIE_AGE = 3600 * 30 # one month
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'dynamic_preferences',
|
||||
'dynamic_preferences.users.apps.UserPreferencesConfig',
|
||||
'YtManagerApp.apps.YtManagerAppConfig',
|
||||
'crispy_forms',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
@ -58,6 +61,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.media',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'dynamic_preferences.processors.global_preferences',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -114,6 +118,7 @@ MEDIA_URL = '/media/'
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
|
||||
LOG_FORMAT = '%(asctime)s|%(process)d|%(thread)d|%(name)s|%(filename)s|%(lineno)d|%(levelname)s|%(message)s'
|
||||
CONSOLE_LOG_FORMAT = '%(asctime)s | %(name)s | %(filename)s:%(lineno)d | %(levelname)s | %(message)s'
|
||||
|
||||
#
|
||||
# Directories
|
||||
@ -121,19 +126,14 @@ LOG_FORMAT = '%(asctime)s|%(process)d|%(thread)d|%(name)s|%(filename)s|%(lineno)
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
PROJECT_ROOT = up(up(os.path.dirname(__file__))) # Project root
|
||||
BASE_DIR = os.path.join(PROJECT_ROOT, "app") # Base dir of the application
|
||||
CONFIG_DIR = os.path.join(PROJECT_ROOT, "config")
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
|
||||
BASE_DIR = up(os.path.dirname(__file__)) # Base dir of the application
|
||||
|
||||
CONFIG_DIR = os.getenv("YTSM_CONFIG_DIR", os.path.join(PROJECT_ROOT, "config"))
|
||||
DATA_DIR = os.getenv("YTSM_DATA_DIR", os.path.join(PROJECT_ROOT, "data"))
|
||||
|
||||
STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
|
||||
MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
|
||||
|
||||
_DEFAULT_CONFIG_DIR = os.path.join(BASE_DIR, "default")
|
||||
_DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.ini')
|
||||
_DEFAULT_LOG_FILE = os.path.join(DATA_DIR, 'log.log')
|
||||
_DEFAULT_MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
|
||||
|
||||
DEFAULTS_FILE = os.path.join(_DEFAULT_CONFIG_DIR, 'defaults.ini')
|
||||
CONFIG_FILE = os.getenv('YTSM_CONFIG_FILE', _DEFAULT_CONFIG_FILE)
|
||||
DATA_CONFIG_FILE = os.path.join(DATA_DIR, 'config.ini')
|
||||
|
||||
#
|
||||
# Defaults
|
||||
@ -141,7 +141,6 @@ DATA_CONFIG_FILE = os.path.join(DATA_DIR, 'config.ini')
|
||||
_DEFAULT_DEBUG = False
|
||||
|
||||
_DEFAULT_SECRET_KEY = '^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0'
|
||||
_DEFAULT_YOUTUBE_API_KEY = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
|
||||
|
||||
_DEFAULT_DATABASE = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
@ -152,13 +151,25 @@ _DEFAULT_DATABASE = {
|
||||
'PORT': None,
|
||||
}
|
||||
|
||||
_SCHEDULER_SYNC_SCHEDULE = '5 * * * *'
|
||||
_DEFAULT_SCHEDULER_CONCURRENCY = 1
|
||||
|
||||
|
||||
CONFIG_ERRORS = []
|
||||
CONFIG_WARNINGS = []
|
||||
|
||||
# These are just to make inspector happy, they will be set in the load_config_ini() method
|
||||
DEBUG = None
|
||||
SECRET_KEY = None
|
||||
DATABASES = None
|
||||
LOG_LEVEL = None
|
||||
|
||||
#
|
||||
# Config parser options
|
||||
#
|
||||
CFG_PARSER_OPTS = {
|
||||
'PROJECT_ROOT': PROJECT_ROOT,
|
||||
'BASE_DIR': BASE_DIR,
|
||||
'CONFIG_DIR': CONFIG_DIR,
|
||||
'DATA_DIR': DATA_DIR,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Load globals from config.ini
|
||||
@ -170,6 +181,8 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa
|
||||
2. config parser
|
||||
3. fallback
|
||||
|
||||
:param integer:
|
||||
:param cfgparser:
|
||||
:param name:
|
||||
:param env_variable:
|
||||
:param fallback:
|
||||
@ -193,7 +206,7 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa
|
||||
# Get from config parser
|
||||
if boolean:
|
||||
try:
|
||||
return cfgparser.getboolean('global', name, fallback=fallback)
|
||||
return cfgparser.getboolean('global', name, fallback=fallback, vars=CFG_PARSER_OPTS)
|
||||
except ValueError:
|
||||
CONFIG_WARNINGS.append(f'config.ini file: Value set for option global.{name} is not valid! '
|
||||
f'Valid options: true, false, on, off.')
|
||||
@ -201,12 +214,12 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa
|
||||
|
||||
if integer:
|
||||
try:
|
||||
return cfgparser.getint('global', name, fallback=fallback)
|
||||
return cfgparser.getint('global', name, fallback=fallback, vars=CFG_PARSER_OPTS)
|
||||
except ValueError:
|
||||
CONFIG_WARNINGS.append(f'config.ini file: Value set for option global.{name} must be an integer number! ')
|
||||
return fallback
|
||||
|
||||
return cfgparser.get('global', name, fallback=fallback)
|
||||
return cfgparser.get('global', name, fallback=fallback, vars=CFG_PARSER_OPTS)
|
||||
|
||||
|
||||
def load_config_ini():
|
||||
@ -214,26 +227,30 @@ def load_config_ini():
|
||||
from YtManagerApp.utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv
|
||||
import dj_database_url
|
||||
|
||||
cfg = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolatorWithEnv())
|
||||
read_ok = cfg.read([DEFAULTS_FILE, CONFIG_FILE, DATA_CONFIG_FILE])
|
||||
try:
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
logging.info(f"Using data directory {DATA_DIR}")
|
||||
except OSError as e:
|
||||
print(f'CRITICAL ERROR! Cannot create data directory {DATA_DIR}! {e}', file=sys.stderr)
|
||||
return
|
||||
|
||||
if DEFAULTS_FILE not in read_ok:
|
||||
CONFIG_ERRORS.append(f'File {DEFAULTS_FILE} could not be read! Please make sure the file is in the right place,'
|
||||
f' and it has read permissions.')
|
||||
cfg = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolatorWithEnv())
|
||||
|
||||
cfg_file = os.path.join(CONFIG_DIR, "config.ini")
|
||||
read_ok = cfg.read([cfg_file])
|
||||
|
||||
if cfg_file not in read_ok:
|
||||
CONFIG_ERRORS.append(f'Configuration file {cfg_file} could not be read! Please make sure the file is in the '
|
||||
'right place, and it has read permissions.')
|
||||
|
||||
# Debug
|
||||
global DEBUG
|
||||
DEBUG = get_global_opt('Debug', cfg, env_variable='YTSM_DEBUG', fallback=_DEFAULT_DEBUG, boolean=True)
|
||||
|
||||
# Media root, which is where thumbnails are stored
|
||||
global MEDIA_ROOT
|
||||
MEDIA_ROOT = get_global_opt('MediaRoot', cfg, env_variable='YTSM_MEDIA_ROOT', fallback=_DEFAULT_MEDIA_ROOT)
|
||||
|
||||
# Keys - secret key, youtube API key
|
||||
# Secret key
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
global SECRET_KEY, YOUTUBE_API_KEY
|
||||
global SECRET_KEY
|
||||
SECRET_KEY = get_global_opt('SecretKey', cfg, env_variable='YTSM_SECRET_KEY', fallback=_DEFAULT_SECRET_KEY)
|
||||
YOUTUBE_API_KEY = get_global_opt('YoutubeApiKey', cfg, env_variable='YTSM_YTAPI_KEY', fallback=_DEFAULT_YOUTUBE_API_KEY)
|
||||
|
||||
# Database
|
||||
global DATABASES
|
||||
@ -242,20 +259,27 @@ def load_config_ini():
|
||||
}
|
||||
|
||||
if cfg.has_option('global', 'DatabaseURL'):
|
||||
DATABASES['default'] = dj_database_url.parse(cfg.get('global', 'DatabaseURL'), conn_max_age=600)
|
||||
DATABASES['default'] = dj_database_url.parse(cfg.get('global', 'DatabaseURL', vars=CFG_PARSER_OPTS),
|
||||
conn_max_age=600)
|
||||
|
||||
else:
|
||||
DATABASES['default'] = {
|
||||
'ENGINE': get_global_opt('DatabaseEngine', cfg, env_variable='YTSM_DB_ENGINE', fallback=_DEFAULT_DATABASE['ENGINE']),
|
||||
'NAME': get_global_opt('DatabaseName', cfg, env_variable='YTSM_DB_NAME', fallback=_DEFAULT_DATABASE['NAME']),
|
||||
'HOST': get_global_opt('DatabaseHost', cfg, env_variable='YTSM_DB_HOST', fallback=_DEFAULT_DATABASE['HOST']),
|
||||
'USER': get_global_opt('DatabaseUser', cfg, env_variable='YTSM_DB_USER', fallback=_DEFAULT_DATABASE['USER']),
|
||||
'PASSWORD': get_global_opt('DatabasePassword', cfg, env_variable='YTSM_DB_PASSWORD', fallback=_DEFAULT_DATABASE['PASSWORD']),
|
||||
'PORT': get_global_opt('DatabasePort', cfg, env_variable='YTSM_DB_PORT', fallback=_DEFAULT_DATABASE['PORT']),
|
||||
'ENGINE': get_global_opt('DatabaseEngine', cfg,
|
||||
env_variable='YTSM_DB_ENGINE', fallback=_DEFAULT_DATABASE['ENGINE']),
|
||||
'NAME': get_global_opt('DatabaseName', cfg,
|
||||
env_variable='YTSM_DB_NAME', fallback=_DEFAULT_DATABASE['NAME']),
|
||||
'HOST': get_global_opt('DatabaseHost', cfg,
|
||||
env_variable='YTSM_DB_HOST', fallback=_DEFAULT_DATABASE['HOST']),
|
||||
'USER': get_global_opt('DatabaseUser', cfg,
|
||||
env_variable='YTSM_DB_USER', fallback=_DEFAULT_DATABASE['USER']),
|
||||
'PASSWORD': get_global_opt('DatabasePassword', cfg,
|
||||
env_variable='YTSM_DB_PASSWORD', fallback=_DEFAULT_DATABASE['PASSWORD']),
|
||||
'PORT': get_global_opt('DatabasePort', cfg,
|
||||
env_variable='YTSM_DB_PORT', fallback=_DEFAULT_DATABASE['PORT']),
|
||||
}
|
||||
|
||||
# Log settings
|
||||
global LOG_LEVEL, LOG_FILE
|
||||
global LOG_LEVEL
|
||||
log_level_str = get_global_opt('LogLevel', cfg, env_variable='YTSM_LOG_LEVEL', fallback='INFO')
|
||||
|
||||
try:
|
||||
@ -266,12 +290,5 @@ def load_config_ini():
|
||||
print("Invalid log level " + LOG_LEVEL)
|
||||
LOG_LEVEL = logging.INFO
|
||||
|
||||
LOG_FILE = get_global_opt('LogFile', cfg, env_variable='YTSM_LOG_FILE', fallback=_DEFAULT_LOG_FILE)
|
||||
|
||||
# Scheduler settings
|
||||
global SCHEDULER_SYNC_SCHEDULE, SCHEDULER_CONCURRENCY
|
||||
SCHEDULER_SYNC_SCHEDULE = get_global_opt('SynchronizationSchedule', cfg, fallback=_SCHEDULER_SYNC_SCHEDULE)
|
||||
SCHEDULER_CONCURRENCY = get_global_opt('SchedulerConcurrency', cfg, fallback=_DEFAULT_SCHEDULER_CONCURRENCY, integer=True)
|
||||
|
||||
|
||||
load_config_ini()
|
||||
|
@ -1,112 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
from collections import ChainMap
|
||||
from configparser import ConfigParser
|
||||
from shutil import copyfile
|
||||
from typing import Optional, Any
|
||||
|
||||
from django.conf import settings as dj_settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .models import UserSettings, Subscription
|
||||
from .utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv
|
||||
|
||||
|
||||
class AppSettings(ConfigParser):
|
||||
_DEFAULT_INTERPOLATION = ExtendedInterpolatorWithEnv()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(allow_no_value=True, *args, **kwargs)
|
||||
|
||||
def initialize(self):
|
||||
self.read([dj_settings.DEFAULTS_FILE, dj_settings.CONFIG_FILE])
|
||||
|
||||
def save(self):
|
||||
if os.path.exists(dj_settings.CONFIG_FILE):
|
||||
# Create a backup
|
||||
copyfile(dj_settings.CONFIG_FILE, dj_settings.CONFIG_FILE + ".backup")
|
||||
else:
|
||||
# Ensure directory exists
|
||||
settings_dir = os.path.dirname(dj_settings.CONFIG_FILE)
|
||||
os.makedirs(settings_dir, exist_ok=True)
|
||||
|
||||
with open(dj_settings.CONFIG_FILE, 'w') as f:
|
||||
self.write(f)
|
||||
|
||||
def __get_combined_dict(self, vars: Optional[Any], sub: Optional[Subscription], user: Optional[User]) -> ChainMap:
|
||||
vars_dict = {}
|
||||
sub_overloads_dict = {}
|
||||
user_settings_dict = {}
|
||||
|
||||
if vars is not None:
|
||||
vars_dict = vars
|
||||
|
||||
if sub is not None:
|
||||
sub_overloads_dict = sub.get_overloads_dict()
|
||||
|
||||
if user is not None:
|
||||
user_settings = UserSettings.find_by_user(user)
|
||||
if user_settings is not None:
|
||||
user_settings_dict = user_settings.to_dict()
|
||||
|
||||
return ChainMap(vars_dict, sub_overloads_dict, user_settings_dict)
|
||||
|
||||
def get_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> str:
|
||||
return super().get(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, None, user))
|
||||
|
||||
def getboolean_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> bool:
|
||||
return super().getboolean(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, None, user))
|
||||
|
||||
def getint_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> int:
|
||||
return super().getint(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, None, user))
|
||||
|
||||
def getfloat_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> float:
|
||||
return super().getfloat(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, None, user))
|
||||
|
||||
def get_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> str:
|
||||
return super().get(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, sub, sub.user))
|
||||
|
||||
def getboolean_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> bool:
|
||||
return super().getboolean(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, sub, sub.user))
|
||||
|
||||
def getint_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> int:
|
||||
return super().getint(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, sub, sub.user))
|
||||
|
||||
def getfloat_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> float:
|
||||
return super().getfloat(section, option,
|
||||
fallback=fallback,
|
||||
vars=self.__get_combined_dict(vars, sub, sub.user))
|
||||
|
||||
|
||||
settings = AppSettings()
|
||||
|
||||
|
||||
def initialize_app_config():
|
||||
settings.initialize()
|
||||
__initialize_logger()
|
||||
logging.info('Application started!')
|
||||
|
||||
|
||||
def __initialize_logger():
|
||||
log_dir = os.path.dirname(dj_settings.LOG_FILE)
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
filename=dj_settings.LOG_FILE,
|
||||
level=dj_settings.LOG_LEVEL,
|
||||
format=dj_settings.LOG_FORMAT)
|
@ -1,11 +1,49 @@
|
||||
from .appconfig import initialize_app_config
|
||||
from .scheduler import initialize_scheduler
|
||||
from .management.jobs.synchronize import schedule_synchronize_global
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.conf import settings as dj_settings
|
||||
|
||||
from .management.appconfig import appconfig
|
||||
from .management.jobs.synchronize import schedule_synchronize_global
|
||||
from .scheduler import initialize_scheduler
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
|
||||
def __initialize_logger():
|
||||
log_dir = os.path.join(dj_settings.DATA_DIR, 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
handlers = []
|
||||
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
os.path.join(log_dir, "log.log"),
|
||||
maxBytes=1024 * 1024,
|
||||
backupCount=5
|
||||
)
|
||||
file_handler.setLevel(dj_settings.LOG_LEVEL)
|
||||
file_handler.setFormatter(logging.Formatter(dj_settings.LOG_FORMAT))
|
||||
logging.root.addHandler(file_handler)
|
||||
logging.root.setLevel(dj_settings.LOG_LEVEL)
|
||||
|
||||
if dj_settings.DEBUG:
|
||||
console_handler = logging.StreamHandler(stream=sys.stdout)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
console_handler.setFormatter(logging.Formatter(dj_settings.CONSOLE_LOG_FORMAT))
|
||||
logging.root.addHandler(console_handler)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
initialize_app_config()
|
||||
initialize_scheduler()
|
||||
schedule_synchronize_global()
|
||||
__initialize_logger()
|
||||
|
||||
try:
|
||||
if appconfig.initialized:
|
||||
initialize_scheduler()
|
||||
schedule_synchronize_global()
|
||||
except OperationalError:
|
||||
# Settings table is not created when running migrate or makemigrations, so just don't do anything in this case.
|
||||
pass
|
||||
|
||||
logging.info('Initialization complete.')
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.apps import AppConfig
|
||||
import os
|
||||
|
||||
|
||||
class YtManagerAppConfig(AppConfig):
|
||||
|
169
app/YtManagerApp/dynamic_preferences_registry.py
Normal file
@ -0,0 +1,169 @@
|
||||
from dynamic_preferences.types import BooleanPreference, StringPreference, IntegerPreference, ChoicePreference
|
||||
from dynamic_preferences.preferences import Section
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
from dynamic_preferences.users.registries import user_preferences_registry
|
||||
|
||||
from YtManagerApp.models import VIDEO_ORDER_CHOICES
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
# we create some section objects to link related preferences together
|
||||
|
||||
hidden = Section('hidden')
|
||||
general = Section('general')
|
||||
scheduler = Section('scheduler')
|
||||
|
||||
|
||||
# Hidden settings
|
||||
@global_preferences_registry.register
|
||||
class Initialized(BooleanPreference):
|
||||
section = hidden
|
||||
name = 'initialized'
|
||||
default = False
|
||||
|
||||
|
||||
# General settings
|
||||
@global_preferences_registry.register
|
||||
class YouTubeAPIKey(StringPreference):
|
||||
section = general
|
||||
name = 'youtube_api_key'
|
||||
default = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
|
||||
required = True
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class AllowRegistrations(BooleanPreference):
|
||||
section = general
|
||||
name = 'allow_registrations'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class SyncSchedule(StringPreference):
|
||||
section = scheduler
|
||||
name = 'synchronization_schedule'
|
||||
default = '5 * * * *' # hourly
|
||||
required = True
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class SchedulerConcurrency(IntegerPreference):
|
||||
section = scheduler
|
||||
name = 'concurrency'
|
||||
default = 2
|
||||
required = True
|
||||
|
||||
|
||||
# User settings
|
||||
@user_preferences_registry.register
|
||||
class MarkDeletedAsWatched(BooleanPreference):
|
||||
name = 'mark_deleted_as_watched'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class AutoDeleteWatched(BooleanPreference):
|
||||
name = 'automatically_delete_watched'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class AutoDownloadEnabled(BooleanPreference):
|
||||
name = 'auto_download'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadGlobalLimit(IntegerPreference):
|
||||
name = 'download_global_limit'
|
||||
default = -1
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadGlobalSizeLimit(IntegerPreference):
|
||||
name = 'download_global_size_limit'
|
||||
default = -1
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadSubscriptionLimit(IntegerPreference):
|
||||
name = 'download_subscription_limit'
|
||||
default = 5
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadMaxAttempts(IntegerPreference):
|
||||
name = 'max_download_attempts'
|
||||
default = 3
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadOrder(ChoicePreference):
|
||||
name = 'download_order'
|
||||
choices = VIDEO_ORDER_CHOICES
|
||||
default = 'playlist'
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadPath(StringPreference):
|
||||
name = 'download_path'
|
||||
default = os.path.join(settings.DATA_DIR, 'downloads')
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadFilePattern(StringPreference):
|
||||
name = 'download_file_pattern'
|
||||
default = '${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]'
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadFormat(StringPreference):
|
||||
name = 'download_format'
|
||||
default = 'bestvideo+bestaudio'
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadSubtitles(BooleanPreference):
|
||||
name = 'download_subtitles'
|
||||
default = True
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadAutogeneratedSubtitles(BooleanPreference):
|
||||
name = 'download_autogenerated_subtitles'
|
||||
default = False
|
||||
required = True
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadAllSubtitles(BooleanPreference):
|
||||
name = 'download_subtitles_all'
|
||||
default = False
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadSubtitlesLangs(StringPreference):
|
||||
name = 'download_subtitles_langs'
|
||||
default = 'en,ro'
|
||||
required = False
|
||||
|
||||
|
||||
@user_preferences_registry.register
|
||||
class DownloadSubtitlesFormat(StringPreference):
|
||||
name = 'download_subtitles_format'
|
||||
default = ''
|
||||
required = False
|
41
app/YtManagerApp/management/appconfig.py
Normal file
@ -0,0 +1,41 @@
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
from YtManagerApp.dynamic_preferences_registry import Initialized, YouTubeAPIKey, AllowRegistrations, SyncSchedule, SchedulerConcurrency
|
||||
|
||||
|
||||
class AppConfig(object):
|
||||
# Properties
|
||||
props = {
|
||||
'initialized': Initialized,
|
||||
'youtube_api_key': YouTubeAPIKey,
|
||||
'allow_registrations': AllowRegistrations,
|
||||
'sync_schedule': SyncSchedule,
|
||||
'concurrency': SchedulerConcurrency
|
||||
}
|
||||
|
||||
# Init
|
||||
def __init__(self, pref_manager):
|
||||
self.__pref_manager = pref_manager
|
||||
|
||||
def __getattr__(self, item):
|
||||
prop_class = AppConfig.props[item]
|
||||
prop_full_name = prop_class.section.name + "__" + prop_class.name
|
||||
return self.__pref_manager[prop_full_name]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key in AppConfig.props:
|
||||
prop_class = AppConfig.props[key]
|
||||
prop_full_name = prop_class.section.name + "__" + prop_class.name
|
||||
self.__pref_manager[prop_full_name] = value
|
||||
else:
|
||||
super().__setattr__(key, value)
|
||||
|
||||
def for_sub(self, subscription, pref: str):
|
||||
value = getattr(subscription, pref)
|
||||
if value is None:
|
||||
value = subscription.user.preferences[pref]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
global_prefs = global_preferences_registry.manager()
|
||||
appconfig = AppConfig(global_prefs)
|
@ -1,6 +1,6 @@
|
||||
from YtManagerApp.appconfig import settings
|
||||
from YtManagerApp.management.jobs.download_video import schedule_download_video
|
||||
from YtManagerApp.models import Video, Subscription, VIDEO_ORDER_MAPPING
|
||||
from YtManagerApp.utils import first_non_null
|
||||
from django.conf import settings as srv_settings
|
||||
import logging
|
||||
import requests
|
||||
@ -12,17 +12,12 @@ log = logging.getLogger('downloader')
|
||||
|
||||
|
||||
def __get_subscription_config(sub: Subscription):
|
||||
enabled = settings.getboolean_sub(sub, 'user', 'AutoDownload')
|
||||
user = sub.user
|
||||
|
||||
global_limit = -1
|
||||
if len(settings.get_sub(sub, 'user', 'DownloadGlobalLimit')) > 0:
|
||||
global_limit = settings.getint_sub(sub, 'user', 'DownloadGlobalLimit')
|
||||
|
||||
limit = -1
|
||||
if len(settings.get_sub(sub, 'user', 'DownloadSubscriptionLimit')) > 0:
|
||||
limit = settings.getint_sub(sub, 'user', 'DownloadSubscriptionLimit')
|
||||
|
||||
order = settings.get_sub(sub, 'user', 'DownloadOrder')
|
||||
enabled = first_non_null(sub.auto_download, user.preferences['auto_download'])
|
||||
global_limit = user.preferences['download_global_limit']
|
||||
limit = first_non_null(sub.download_limit, user.preferences['download_subscription_limit'])
|
||||
order = first_non_null(sub.download_order, user.preferences['download_order'])
|
||||
order = VIDEO_ORDER_MAPPING[order]
|
||||
|
||||
return enabled, global_limit, limit, order
|
||||
|
@ -1,12 +1,14 @@
|
||||
from YtManagerApp.models import Video
|
||||
from YtManagerApp import scheduler
|
||||
from YtManagerApp.appconfig import settings
|
||||
import os
|
||||
import youtube_dl
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from string import Template
|
||||
from threading import Lock
|
||||
|
||||
import youtube_dl
|
||||
|
||||
from YtManagerApp import scheduler
|
||||
from YtManagerApp.models import Video
|
||||
|
||||
log = logging.getLogger('video_downloader')
|
||||
log_youtube_dl = log.getChild('youtube_dl')
|
||||
|
||||
@ -25,9 +27,8 @@ def __get_valid_path(path):
|
||||
return value
|
||||
|
||||
|
||||
def __build_youtube_dl_params(video: Video):
|
||||
# resolve path
|
||||
pattern_dict = {
|
||||
def __build_template_dict(video: Video):
|
||||
return {
|
||||
'channel': video.subscription.channel_name,
|
||||
'channel_id': video.subscription.channel_id,
|
||||
'playlist': video.subscription.name,
|
||||
@ -37,22 +38,30 @@ def __build_youtube_dl_params(video: Video):
|
||||
'id': video.video_id,
|
||||
}
|
||||
|
||||
download_path = settings.get_sub(video.subscription, 'user', 'DownloadPath')
|
||||
output_pattern = __get_valid_path(settings.get_sub(
|
||||
video.subscription, 'user', 'DownloadFilePattern', vars=pattern_dict))
|
||||
|
||||
def __build_youtube_dl_params(video: Video):
|
||||
|
||||
sub = video.subscription
|
||||
user = sub.user
|
||||
|
||||
# resolve path
|
||||
download_path = user.preferences['download_path']
|
||||
|
||||
template_dict = __build_template_dict(video)
|
||||
output_pattern = Template(user.preferences['download_file_pattern']).safe_substitute(template_dict)
|
||||
|
||||
output_path = os.path.join(download_path, output_pattern)
|
||||
output_path = os.path.normpath(output_path)
|
||||
|
||||
youtube_dl_params = {
|
||||
'logger': log_youtube_dl,
|
||||
'format': settings.get_sub(video.subscription, 'user', 'DownloadFormat'),
|
||||
'format': user.preferences['download_format'],
|
||||
'outtmpl': output_path,
|
||||
'writethumbnail': True,
|
||||
'writedescription': True,
|
||||
'writesubtitles': settings.getboolean_sub(video.subscription, 'user', 'DownloadSubtitles'),
|
||||
'writeautomaticsub': settings.getboolean_sub(video.subscription, 'user', 'DownloadAutogeneratedSubtitles'),
|
||||
'allsubtitles': settings.getboolean_sub(video.subscription, 'user', 'DownloadSubtitlesAll'),
|
||||
'writesubtitles': user.preferences['download_subtitles'],
|
||||
'writeautomaticsub': user.preferences['download_autogenerated_subtitles'],
|
||||
'allsubtitles': user.preferences['download_subtitles_all'],
|
||||
'postprocessors': [
|
||||
{
|
||||
'key': 'FFmpegMetadata'
|
||||
@ -60,12 +69,12 @@ def __build_youtube_dl_params(video: Video):
|
||||
]
|
||||
}
|
||||
|
||||
sub_langs = settings.get_sub(video.subscription, 'user', 'DownloadSubtitlesLangs').split(',')
|
||||
sub_langs = user.preferences['download_subtitles_langs'].split(',')
|
||||
sub_langs = [i.strip() for i in sub_langs]
|
||||
if len(sub_langs) > 0:
|
||||
youtube_dl_params['subtitleslangs'] = sub_langs
|
||||
|
||||
sub_format = settings.get_sub(video.subscription, 'user', 'DownloadSubtitlesFormat')
|
||||
sub_format = user.preferences['download_subtitles_format']
|
||||
if len(sub_format) > 0:
|
||||
youtube_dl_params['subtitlesformat'] = sub_format
|
||||
|
||||
@ -74,6 +83,8 @@ def __build_youtube_dl_params(video: Video):
|
||||
|
||||
def download_video(video: Video, attempt: int = 1):
|
||||
|
||||
user = video.subscription.user
|
||||
|
||||
log.info('Downloading video %d [%s %s]', video.id, video.video_id, video.name)
|
||||
|
||||
# Issue: if multiple videos are downloaded at the same time, a race condition appears in the mkdirs() call that
|
||||
@ -82,7 +93,7 @@ def download_video(video: Video, attempt: int = 1):
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
max_attempts = settings.getint_sub(video.subscription, 'user', 'DownloadMaxAttempts', fallback=3)
|
||||
max_attempts = user.preferences['max_download_attempts']
|
||||
|
||||
youtube_dl_params, output_path = __build_youtube_dl_params(video)
|
||||
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
|
||||
|
@ -4,8 +4,8 @@ from threading import Lock
|
||||
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from YtManagerApp import scheduler
|
||||
from YtManagerApp.appconfig import settings
|
||||
from YtManagerApp.scheduler import scheduler
|
||||
from YtManagerApp.management.appconfig import appconfig
|
||||
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription
|
||||
from YtManagerApp.models import *
|
||||
from YtManagerApp.utils import youtube
|
||||
@ -43,6 +43,8 @@ def __check_new_videos_sub(subscription: Subscription, yt_api: youtube.YoutubeAP
|
||||
|
||||
def __detect_deleted(subscription: Subscription):
|
||||
|
||||
user = subscription.user
|
||||
|
||||
for video in Video.objects.filter(subscription=subscription, downloaded_path__isnull=False):
|
||||
found_video = False
|
||||
files = []
|
||||
@ -71,7 +73,7 @@ def __detect_deleted(subscription: Subscription):
|
||||
video.downloaded_path = None
|
||||
|
||||
# Mark watched?
|
||||
if settings.getboolean_sub(subscription, 'user', 'MarkDeletedAsWatched'):
|
||||
if user.preferences['mark_deleted_as_watched']:
|
||||
video.watched = True
|
||||
|
||||
video.save()
|
||||
@ -146,17 +148,29 @@ def synchronize_subscription(subscription: Subscription):
|
||||
__lock.release()
|
||||
|
||||
|
||||
__global_sync_job = None
|
||||
|
||||
|
||||
def schedule_synchronize_global():
|
||||
trigger = CronTrigger.from_crontab(settings.get('global', 'SynchronizationSchedule'))
|
||||
job = scheduler.scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
|
||||
log.info('Scheduled synchronize job job=%s', job.id)
|
||||
global __global_sync_job
|
||||
|
||||
trigger = CronTrigger.from_crontab(appconfig.sync_schedule)
|
||||
|
||||
if __global_sync_job is None:
|
||||
trigger = CronTrigger.from_crontab(appconfig.sync_schedule)
|
||||
__global_sync_job = scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
|
||||
|
||||
else:
|
||||
__global_sync_job.reschedule(trigger, max_instances=1, coalesce=True)
|
||||
|
||||
log.info('Scheduled synchronize job job=%s', __global_sync_job.id)
|
||||
|
||||
|
||||
def schedule_synchronize_now():
|
||||
job = scheduler.scheduler.add_job(synchronize, max_instances=1, coalesce=True)
|
||||
job = scheduler.add_job(synchronize, max_instances=1, coalesce=True)
|
||||
log.info('Scheduled synchronize now job job=%s', job.id)
|
||||
|
||||
|
||||
def schedule_synchronize_now_subscription(subscription: Subscription):
|
||||
job = scheduler.scheduler.add_job(synchronize_subscription, args=[subscription])
|
||||
job = scheduler.add_job(synchronize_subscription, args=[subscription])
|
||||
log.info('Scheduled synchronize subscription job subscription=(%s), job=%s', subscription, job.id)
|
||||
|
25
app/YtManagerApp/migrations/0008_auto_20181229_2035.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.1.2 on 2018-12-29 20:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('YtManagerApp', '0007_auto_20181029_1638'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='usersettings',
|
||||
name='user',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='subscription',
|
||||
old_name='delete_after_watched',
|
||||
new_name='automatically_delete_watched',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserSettings',
|
||||
),
|
||||
]
|
@ -1,11 +1,11 @@
|
||||
import logging
|
||||
from typing import Callable, Union, Any, Optional
|
||||
import os
|
||||
from typing import Callable, Union, Any, Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.functions import Lower
|
||||
|
||||
from YtManagerApp.utils import youtube
|
||||
|
||||
# help_text = user shown text
|
||||
@ -30,136 +30,6 @@ VIDEO_ORDER_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
class UserSettings(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
mark_deleted_as_watched = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='When a downloaded video is deleted from the system, it will be marked as \'watched\'.')
|
||||
|
||||
delete_watched = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='Videos marked as watched are automatically deleted.')
|
||||
|
||||
auto_download = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='Enables or disables automatic downloading.')
|
||||
|
||||
download_global_limit = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text='Limits the total number of videos downloaded (-1 = no limit).')
|
||||
|
||||
download_subscription_limit = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
help_text='Limits the number of videos downloaded per subscription (-1 = no limit). '
|
||||
' This setting can be overriden for each individual subscription in the subscription edit dialog.')
|
||||
|
||||
download_order = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=100,
|
||||
choices=VIDEO_ORDER_CHOICES,
|
||||
help_text='The order in which videos will be downloaded.'
|
||||
)
|
||||
|
||||
download_path = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=1024,
|
||||
help_text='Path on the disk where downloaded videos are stored. '
|
||||
' You can use environment variables using syntax: <code>${env:...}</code>'
|
||||
)
|
||||
|
||||
download_file_pattern = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=1024,
|
||||
help_text='A pattern which describes how downloaded files are organized. Extensions are automatically appended.'
|
||||
' You can use the following fields, using the <code>${field}</code> syntax:'
|
||||
' channel, channel_id, playlist, playlist_id, playlist_index, title, id.'
|
||||
' Example: <code>${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]</code>')
|
||||
|
||||
download_format = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=256,
|
||||
help_text='Download format that will be passed to youtube-dl. '
|
||||
' See the <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#format-selection">'
|
||||
' youtube-dl documentation</a> for more details.')
|
||||
|
||||
download_subtitles = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='Enable downloading subtitles for the videos.'
|
||||
' The flag is passed directly to youtube-dl. You can find more information'
|
||||
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.')
|
||||
|
||||
download_autogenerated_subtitles = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='Enables downloading the automatically generated subtitle.'
|
||||
' The flag is passed directly to youtube-dl. You can find more information'
|
||||
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.')
|
||||
|
||||
download_subtitles_all = models.BooleanField(
|
||||
null=True, blank=True,
|
||||
help_text='If enabled, all the subtitles in all the available languages will be downloaded.'
|
||||
' The flag is passed directly to youtube-dl. You can find more information'
|
||||
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.')
|
||||
|
||||
download_subtitles_langs = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=250,
|
||||
help_text='Comma separated list of languages for which subtitles will be downloaded.'
|
||||
' The flag is passed directly to youtube-dl. You can find more information'
|
||||
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.')
|
||||
|
||||
download_subtitles_format = models.CharField(
|
||||
null=True, blank=True,
|
||||
max_length=100,
|
||||
help_text='Subtitles format preference. Examples: srt/ass/best'
|
||||
' The flag is passed directly to youtube-dl. You can find more information'
|
||||
' <a href="https://github.com/rg3/youtube-dl/blob/master/README.md#subtitle-options">here</a>.')
|
||||
|
||||
@staticmethod
|
||||
def find_by_user(user: User):
|
||||
result = UserSettings.objects.filter(user=user)
|
||||
if len(result) > 0:
|
||||
return result.first()
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
|
||||
if self.mark_deleted_as_watched is not None:
|
||||
ret['MarkDeletedAsWatched'] = self.mark_deleted_as_watched
|
||||
if self.delete_watched is not None:
|
||||
ret['DeleteWatched'] = self.delete_watched
|
||||
if self.auto_download is not None:
|
||||
ret['AutoDownload'] = self.auto_download
|
||||
if self.download_global_limit is not None:
|
||||
ret['DownloadGlobalLimit'] = self.download_global_limit
|
||||
if self.download_subscription_limit is not None:
|
||||
ret['DownloadSubscriptionLimit'] = self.download_subscription_limit
|
||||
if self.download_order is not None:
|
||||
ret['DownloadOrder'] = self.download_order
|
||||
if self.download_path is not None:
|
||||
ret['DownloadPath'] = self.download_path
|
||||
if self.download_file_pattern is not None:
|
||||
ret['DownloadFilePattern'] = self.download_file_pattern
|
||||
if self.download_format is not None:
|
||||
ret['DownloadFormat'] = self.download_format
|
||||
if self.download_subtitles is not None:
|
||||
ret['DownloadSubtitles'] = self.download_subtitles
|
||||
if self.download_autogenerated_subtitles is not None:
|
||||
ret['DownloadAutogeneratedSubtitles'] = self.download_autogenerated_subtitles
|
||||
if self.download_subtitles_all is not None:
|
||||
ret['DownloadSubtitlesAll'] = self.download_subtitles_all
|
||||
if self.download_subtitles_langs is not None:
|
||||
ret['DownloadSubtitlesLangs'] = self.download_subtitles_langs
|
||||
if self.download_subtitles_format is not None:
|
||||
ret['DownloadSubtitlesFormat'] = self.download_subtitles_format
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class SubscriptionFolder(models.Model):
|
||||
name = models.CharField(null=False, max_length=250)
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
|
||||
@ -246,7 +116,7 @@ class Subscription(models.Model):
|
||||
null=True, blank=True,
|
||||
max_length=128,
|
||||
choices=VIDEO_ORDER_CHOICES)
|
||||
delete_after_watched = models.BooleanField(null=True, blank=True)
|
||||
automatically_delete_watched = models.BooleanField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -291,18 +161,6 @@ class Subscription(models.Model):
|
||||
def delete_subscription(self, keep_downloaded_videos: bool):
|
||||
self.delete()
|
||||
|
||||
def get_overloads_dict(self) -> dict:
|
||||
d = {}
|
||||
if self.auto_download is not None:
|
||||
d['AutoDownload'] = self.auto_download
|
||||
if self.download_limit is not None:
|
||||
d['DownloadSubscriptionLimit'] = self.download_limit
|
||||
if self.download_order is not None:
|
||||
d['DownloadOrder'] = self.download_order
|
||||
if self.delete_after_watched is not None:
|
||||
d['DeleteWatched'] = self.delete_after_watched
|
||||
return d
|
||||
|
||||
|
||||
class Video(models.Model):
|
||||
video_id = models.TextField(null=False)
|
||||
@ -339,11 +197,11 @@ class Video(models.Model):
|
||||
self.watched = True
|
||||
self.save()
|
||||
if self.downloaded_path is not None:
|
||||
from YtManagerApp.appconfig import settings
|
||||
from YtManagerApp.management.appconfig import appconfig
|
||||
from YtManagerApp.management.jobs.delete_video import schedule_delete_video
|
||||
from YtManagerApp.management.jobs.synchronize import schedule_synchronize_now_subscription
|
||||
|
||||
if settings.getboolean_sub(self.subscription, 'user', 'DeleteWatched'):
|
||||
if appconfig.for_sub(self.subscription, 'automatically_delete_watched'):
|
||||
schedule_delete_video(self)
|
||||
schedule_synchronize_now_subscription(self.subscription)
|
||||
|
||||
@ -363,13 +221,13 @@ class Video(models.Model):
|
||||
def delete_files(self):
|
||||
if self.downloaded_path is not None:
|
||||
from YtManagerApp.management.jobs.delete_video import schedule_delete_video
|
||||
from YtManagerApp.appconfig import settings
|
||||
from YtManagerApp.management.appconfig import appconfig
|
||||
from YtManagerApp.management.jobs.synchronize import schedule_synchronize_now_subscription
|
||||
|
||||
schedule_delete_video(self)
|
||||
|
||||
# Mark watched?
|
||||
if settings.getboolean_sub(self, 'user', 'MarkDeletedAsWatched'):
|
||||
if self.subscription.user.preferences['mark_deleted_as_watched']:
|
||||
self.watched = True
|
||||
schedule_synchronize_now_subscription(self.subscription)
|
||||
|
||||
|
@ -1,24 +1,27 @@
|
||||
import logging
|
||||
import sys
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from django.conf import settings
|
||||
|
||||
scheduler: BackgroundScheduler = None
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from YtManagerApp.management.appconfig import appconfig
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
|
||||
def initialize_scheduler():
|
||||
global scheduler
|
||||
|
||||
if scheduler.running:
|
||||
return
|
||||
|
||||
logger = logging.getLogger('scheduler')
|
||||
executors = {
|
||||
'default': {
|
||||
'type': 'threadpool',
|
||||
'max_workers': settings.SCHEDULER_CONCURRENCY
|
||||
'max_workers': appconfig.concurrency
|
||||
}
|
||||
}
|
||||
job_defaults = {
|
||||
'misfire_grace_time': 60 * 60 * 24 * 365 # 1 year
|
||||
}
|
||||
|
||||
scheduler = BackgroundScheduler(logger=logger, executors=executors, job_defaults=job_defaults)
|
||||
scheduler.configure(logger=logger, executors=executors, job_defaults=job_defaults)
|
||||
scheduler.start()
|
||||
|
@ -1,5 +1,6 @@
|
||||
#main_body {
|
||||
margin-bottom: 4rem; }
|
||||
margin-bottom: 4rem;
|
||||
margin-top: 0; }
|
||||
|
||||
#main_footer {
|
||||
position: fixed;
|
||||
@ -8,7 +9,7 @@
|
||||
bottom: 0;
|
||||
height: 2rem;
|
||||
line-height: 2rem;
|
||||
padding: 0rem 1rem;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
font-size: 10pt; }
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "AAEA,UAAW;EACP,aAAa,EAAE,IAAI;;AAGvB,YAAa;EACT,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAqBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAO7B,8BAAU;EACN,KAAK,EAAE,KAAK;AAKpB,8BAAgB;EACZ,KAAK,EAvHE,OAAO;AAyHlB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC;;AAIjB,YAAa;EACT,OAAO,EAAE,YAAY;EACrB,aAAa,EAAE,MAAM;;AAGzB,YAAa;EACT,MAAM,EAAE,OAAO;EACf,iBAAK;IACD,OAAO,EAAE,cAAc;IACvB,SAAS,EAAE,IAAI",
|
||||
"mappings": "AAEA,UAAW;EACP,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,CAAC;;AAGjB,YAAa;EACT,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,MAAM;EACf,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAqBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAO7B,8BAAU;EACN,KAAK,EAAE,KAAK;AAKpB,8BAAgB;EACZ,KAAK,EAxHE,OAAO;AA0HlB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC;;AAIjB,YAAa;EACT,OAAO,EAAE,YAAY;EACrB,aAAa,EAAE,MAAM;;AAGzB,YAAa;EACT,MAAM,EAAE,OAAO;EACf,iBAAK;IACD,OAAO,EAAE,cAAc;IACvB,SAAS,EAAE,IAAI",
|
||||
"sources": ["style.scss"],
|
||||
"names": [],
|
||||
"file": "style.css"
|
||||
|
@ -2,6 +2,7 @@ $accent-color: #007bff;
|
||||
|
||||
#main_body {
|
||||
margin-bottom: 4rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#main_footer {
|
||||
@ -11,7 +12,7 @@ $accent-color: #007bff;
|
||||
bottom: 0;
|
||||
height: 2rem;
|
||||
line-height: 2rem;
|
||||
padding: 0rem 1rem;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
font-size: 10pt;
|
||||
|
BIN
app/YtManagerApp/static/YtManagerApp/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 12 KiB |
3719
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap-grid.css
vendored
Normal file
7
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap-grid.min.css
vendored
Normal file
319
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,319 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
8
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap-reboot.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
9887
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap.css
vendored
Normal file
7
app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap.min.css
vendored
Normal file
6818
app/YtManagerApp/static/YtManagerApp/import/bootstrap/js/bootstrap.bundle.js
vendored
Normal file
7
app/YtManagerApp/static/YtManagerApp/import/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
4249
app/YtManagerApp/static/YtManagerApp/import/bootstrap/js/bootstrap.js
vendored
Normal file
7
app/YtManagerApp/static/YtManagerApp/import/bootstrap/js/bootstrap.min.js
vendored
Normal file
10364
app/YtManagerApp/static/YtManagerApp/import/jquery/jquery-3.3.1.js
vendored
Normal file
2
app/YtManagerApp/static/YtManagerApp/import/jquery/jquery-3.3.1.min.js
vendored
Normal file
14
app/YtManagerApp/static/YtManagerApp/import/jstree/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/debug
|
||||
/jstree.sublime-project
|
||||
/jstree.sublime-workspace
|
||||
/bower_components
|
||||
/node_modules
|
||||
/site
|
||||
/nuget
|
||||
/demo/filebrowser/data/root
|
||||
/npm.txt
|
||||
/libs
|
||||
/docs
|
||||
/dist/libs
|
||||
/.vscode
|
||||
/.idea
|
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2014 Ivan Bozhanov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
663
app/YtManagerApp/static/YtManagerApp/import/jstree/README.md
Normal file
@ -0,0 +1,663 @@
|
||||
# jstree
|
||||
|
||||
[jsTree](http://www.jstree.com/) is jquery plugin, that provides interactive trees. It is absolutely free, [open source](https://github.com/vakata/jstree) and distributed under the MIT license.
|
||||
|
||||
jsTree is easily extendable, themable and configurable, it supports HTML & JSON data sources, AJAX & async callback loading.
|
||||
|
||||
jsTree functions properly in either box-model (content-box or border-box), can be loaded as an AMD module, and has a built in mobile theme for responsive design, that can easily be customized. It uses jQuery's event system, so binding callbacks on various events in the tree is familiar and easy.
|
||||
|
||||
You also get:
|
||||
* drag & drop support
|
||||
* keyboard navigation
|
||||
* inline edit, create and delete
|
||||
* tri-state checkboxes
|
||||
* fuzzy searching
|
||||
* customizable node types
|
||||
|
||||
_Aside from this readme you can find a lot more info on [jstree.com](http://www.jstree.com) & [the discussion group](https://groups.google.com/forum/#!forum/jstree)_.
|
||||
|
||||
---
|
||||
|
||||
<!-- MarkdownTOC depth=0 autolink=true bracket=round -->
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Include all neccessary files](#include-all-neccessary-files)
|
||||
- [Populating a tree using HTML](#populating-a-tree-using-html)
|
||||
- [Populating a tree using an array \(or JSON\)](#populating-a-tree-using-an-array-or-json)
|
||||
- [The required JSON format](#the-required-json-format)
|
||||
- [Populating the tree using AJAX](#populating-the-tree-using-ajax)
|
||||
- [Populating the tree using AJAX and lazy loading nodes](#populating-the-tree-using-ajax-and-lazy-loading-nodes)
|
||||
- [Populating the tree using a callback function](#populating-the-tree-using-a-callback-function)
|
||||
- [Working with events](#working-with-events)
|
||||
- [Interacting with the tree using the API](#interacting-with-the-tree-using-the-api)
|
||||
- [More on configuration](#more-on-configuration)
|
||||
- [Plugins](#plugins)
|
||||
- [checkbox](#checkbox)
|
||||
- [contextmenu](#contextmenu)
|
||||
- [dnd](#dnd)
|
||||
- [massload](#massload)
|
||||
- [search](#search)
|
||||
- [sort](#sort)
|
||||
- [state](#state)
|
||||
- [types](#types)
|
||||
- [unique](#unique)
|
||||
- [wholerow](#wholerow)
|
||||
- [More plugins](#more-plugins)
|
||||
- [PHP demos moved to new repository](#php-demos-moved-to-new-repository)
|
||||
- [License & Contributing](#license--contributing)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Include all neccessary files
|
||||
To get started you need 3 things in your page:
|
||||
1. jQuery (anything above 1.9.1 will work)
|
||||
2. A jstree theme (there is only one theme supplied by default)
|
||||
3. The jstree source file
|
||||
|
||||
```html
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.5/themes/default/style.min.css" />
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.5/jstree.min.js"></script>
|
||||
```
|
||||
|
||||
_If you decide to host jstree yourself - the files are located in the `dist` folder. You can safely ignore the `dist/libs` folder._
|
||||
|
||||
---
|
||||
|
||||
### Populating a tree using HTML
|
||||
|
||||
Now we are all set to create a tree, inline HTML is the easiest option (suitable for menus). All you need to do is select a node (using a jQuery selector) and invoke the `.jstree()` function to let jstree know you want to render a tree inside the selected node. `$.jstree.create(element)` can be used too.
|
||||
|
||||
```html
|
||||
<div id="container">
|
||||
<ul>
|
||||
<li>Root node
|
||||
<ul>
|
||||
<li>Child node 1</li>
|
||||
<li>Child node 2</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/)
|
||||
|
||||
_You can add a few options when rendering a node using a data-attribute (note the quotes):_
|
||||
```html
|
||||
<li data-jstree='{ "selected" : true, "opened" : true }'>Root node ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Populating a tree using an array (or JSON)
|
||||
|
||||
Building trees from HTML is easy, but it is not very flexible, inline JS data is a better option:
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{ "text" : "Root node", "children" : [
|
||||
{ "text" : "Child node 1" },
|
||||
{ "text" : "Child node 2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4478/)
|
||||
|
||||
Unlike the previous simple HTML example, this time the `.jstree()` function accepts a config object.
|
||||
|
||||
For now it is important to note that jstree will try to parse any data you specify in the `core.data` key and use it to create a tree. As seen in the previous example, if this key is missing jstree will try to parse the inline HTML of the container.
|
||||
|
||||
#### The required JSON format
|
||||
|
||||
The data you use must be in a specific format, each branch of the tree is represented by an object, which must at least have a `text` key. The `children` key can be used to add children to the branch, it should be an array of objects.
|
||||
|
||||
_Keep in mind, you can use a simple string instead of an object if all you need is node with the given text, the above data can be written as:_
|
||||
|
||||
```js
|
||||
[ { "text" : "Root node", "children" : [ "Child node 1", "Child node 2" ] } ]
|
||||
```
|
||||
|
||||
There are other available options for each node, only set them if you need them like:
|
||||
|
||||
* `id` - makes if possible to identify a node later (will also be used as a DOM ID of the `LI` node). _Make sure you do not repeat the same ID in a tree instance (that would defeat its purpose of being a unique identifier and may cause problems for jstree)_.
|
||||
* `icon` - a string which will be used for the node's icon - this can either be a path to a file, or a className (or list of classNames), which you can style in your CSS (font icons also work).
|
||||
* `data` - this can be anything you want - it is metadata you want attached to the node - you will be able to access and modify it any time later - it has no effect on the visuals of the node.
|
||||
* `state` - an object specifyng a few options about the node:
|
||||
- `selected` - if the node should be initially selected
|
||||
- `opened` - if the node should be initially opened
|
||||
- `disabled` - if the node should be disabled
|
||||
- `checked` - __checkbox plugin specific__ - if the node should be checked (only used when `tie_selection` is `false`, which you should only do if you really know what you are doing)
|
||||
- `undetermined` - __checkbox plugin specific__ - if the node should be rendered in undetermined state (only used with lazy loading and when the node is not yet loaded, otherwise this state is automatically calculated).
|
||||
* `type` - __types plugin specific__ - the type of the nodes (should be defined in the types config), if not set `"default"` is assumed.
|
||||
* `li_attr` - object of values which will be used to add HTML attributes on the resulting `LI` DOM node.
|
||||
* `a_attr` - object of values which will be used to add HTML attributes on the resulting `A` node.
|
||||
|
||||
Here is a new demo with some of those properties set:
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{
|
||||
"text" : "Root node",
|
||||
"state" : {"opened" : true },
|
||||
"children" : [
|
||||
{
|
||||
"text" : "Child node 1",
|
||||
"state" : { "selected" : true },
|
||||
"icon" : "glyphicon glyphicon-flash"
|
||||
},
|
||||
{ "text" : "Child node 2", "state" : { "disabled" : true } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4479/)
|
||||
|
||||
---
|
||||
|
||||
### Populating the tree using AJAX
|
||||
|
||||
Building off of the previous example, let's see how to have jstree make AJAX requests for you.
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : {
|
||||
"url" : "//www.jstree.com/fiddle/",
|
||||
"dataType" : "json" // needed only if you do not supply JSON headers
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
The server response is:
|
||||
```json
|
||||
[{
|
||||
"id":1,"text":"Root node","children":[
|
||||
{"id":2,"text":"Child node 1"},
|
||||
{"id":3,"text":"Child node 2"}
|
||||
]
|
||||
}]
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4480/)
|
||||
|
||||
Instead of a JS array, you can set `core.data` to a [jQuery AJAX config](http://api.jquery.com/jQuery.ajax/).
|
||||
jsTree will hit that URL, and provided you return properly formatted JSON it will be displayed.
|
||||
|
||||
_If you cannot provide proper JSON headers, set `core.data.dataType` to `"json"`._
|
||||
|
||||
The ids in the server response make it possible to identify nodes later (which we will see in the next few demos), but they are not required.
|
||||
|
||||
__WHEN USING IDS MAKE SURE THEY ARE UNIQUE INSIDE A PARTICULAR TREE__
|
||||
|
||||
---
|
||||
|
||||
### Populating the tree using AJAX and lazy loading nodes
|
||||
|
||||
Lazy loading means nodes will be loaded when they are needed. Imagine you have a huge amount of nodes you want to show, but loading them with a single request is way too much traffic. Lazy loading makes it possible to load nodes on the fly - jstree will perform AJAX requests as the user browses the tree.
|
||||
|
||||
Here we take our previous example, and lazy load the "Child node 1" node.
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : {
|
||||
"url" : "//www.jstree.com/fiddle/?lazy",
|
||||
"data" : function (node) {
|
||||
return { "id" : node.id };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
The initial server response is:
|
||||
```json
|
||||
[{
|
||||
"id":1,"text":"Root node","children":[
|
||||
{"id":2,"text":"Child node 1","children":true},
|
||||
{"id":3,"text":"Child node 2"}
|
||||
]
|
||||
}]
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4481/)
|
||||
|
||||
Now to focus on what is different. First off the `"data"` config option of the data object. If you check with jQuery, it is supposed to be a string or an object. But jstree makes it possible to set a function.
|
||||
|
||||
Each time jstree needs to make an AJAX call this function will be called and will receive a single parameter - the node that is being loaded. The return value of this function will be used as the actual `"data"` of the AJAX call. To understand better open up the demo and see the requests go off in the console.
|
||||
|
||||
You will notice that the first request goes off to:
|
||||
`http://www.jstree.com/fiddle?lazy&id=#`
|
||||
`#` is the special ID that the function receives when jstree needs to load the root nodes.
|
||||
|
||||
Now go ahead and open the root node - two children will be shown, but no request will be made - that is because we loaded those children along with the first request.
|
||||
|
||||
Onto the next difference - "Child node 1" appears closed - that is because in the data we supplied `true` as the `"children"` property of this node (you can see it in the server response). This special value indicated to jstree, that it has to lazy load the "Child node 1" node.
|
||||
|
||||
Proceed and open this node - you will see a next request fire off to:
|
||||
`http://www.jstree.com/fiddle?lazy&id=2`
|
||||
ID is set to `2` because the node being loaded has an ID of `2`, and we have configured jstree to send the node ID along with the AJAX request (the `data` function).
|
||||
|
||||
The server response is:
|
||||
```json
|
||||
["Child node 3","Child node 4"]
|
||||
```
|
||||
|
||||
_You can also set `"url"` to a function and it works exactly as with `"data"` - each time a request has to be made, jstree will invoke your function and the request will go off to whatever you return in this function. This is useful when dealing with URLs like: `http://example.com/get_children/1`._
|
||||
|
||||
### Populating the tree using a callback function
|
||||
|
||||
Sometimes you may not want jsTree to make AJAX calls for you - you might want to make them yourself, or use some other method of populating the tree. In that case you can use a callback function.
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : function (node, cb) {
|
||||
if(node.id === "#") {
|
||||
cb([{"text" : "Root", "id" : "1", "children" : true}]);
|
||||
}
|
||||
else {
|
||||
cb(["Child"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4482/)
|
||||
|
||||
As you can see your function will receive two arguments - the node whose children need to be loaded and a callback function to call with the data once you have it. The data follows the same familiar JSON format and lazy loading works just as with AJAX (as you can see in the above example).
|
||||
|
||||
---
|
||||
|
||||
## Working with events
|
||||
|
||||
jstree provides a lot of events to let you know something happened with the tree. The events are the same regardless of how you populate the tree.
|
||||
Let's use the most basic event `changed` - it fires when selection on the tree changes:
|
||||
|
||||
```html
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{"id" : 1, "text" : "Node 1"},
|
||||
{"id" : 2, "text" : "Node 2"},
|
||||
]
|
||||
}
|
||||
});
|
||||
$('#container').on("changed.jstree", function (e, data) {
|
||||
console.log("The selected nodes are:");
|
||||
console.log(data.selected);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4483/)
|
||||
|
||||
All jstree events fire in a special `".jstree"` namespace - this is why we listen for `"changed.jstree"`. The handler itself receives one additional parameter - it will be populated with all you need to know about the event that happened. In this case `data.selected` is an array of selected node IDs (please note, that if you have not specified IDs they will be autogenerated).
|
||||
|
||||
Let's extend this a bit and log out the text of the node instead of the ID.
|
||||
|
||||
```js
|
||||
$('#container').on("changed.jstree", function (e, data) {
|
||||
console.log(data.instance.get_selected(true)[0].text);
|
||||
console.log(data.instance.get_node(data.selected[0]).text);
|
||||
});
|
||||
```
|
||||
|
||||
The two rows above achieve exactly the same thing - get the text of the first selected node.
|
||||
|
||||
In the `data` argument object you will always get an `instance` key - that is a reference to the tree instance, so that you can easily invoke methods.
|
||||
|
||||
__All available functions and events are documented in the API docs__
|
||||
|
||||
---
|
||||
|
||||
## Interacting with the tree using the API
|
||||
|
||||
We scratched the surface on interacting with the tree in the previous example. Let's move on to obtaining an instance and calling a method on this instance:
|
||||
|
||||
```html
|
||||
<button>Select node 1</button>
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#container').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{"id" : 1, "text" : "Node 1"},
|
||||
{"id" : 2, "text" : "Node 2"},
|
||||
]
|
||||
}
|
||||
});
|
||||
$('button').on("click", function () {
|
||||
var instance = $('#container').jstree(true);
|
||||
instance.deselect_all();
|
||||
instance.select_node('1');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4484/)
|
||||
|
||||
The above example shows how to obtain a reference to a jstree instance (again with a selector, but this time instead of a config, we pass a boolean `true`), and call a couple of methods - the latter one is selecting a node by its ID.
|
||||
|
||||
Methods can also be invoked like this:
|
||||
|
||||
```js
|
||||
$('#container').jstree("select_node", "1");
|
||||
```
|
||||
|
||||
__All available functions and events are documented in the API docs__
|
||||
|
||||
## More on configuration
|
||||
|
||||
We already covered the config object in general (when we specified inline & AJAX data sources).
|
||||
|
||||
```js
|
||||
$("#tree").jstree({ /* config object goes here */ });
|
||||
```
|
||||
|
||||
Each key in the config object corresponds to a plugin, and the value of that key is the configuration for that plugin. There are also two special keys `"core"` and `"plugins"`:
|
||||
* `"core"` stores the core configuration options
|
||||
* `"plugins"` is an array of plugin names (strings) you want active on the instance
|
||||
|
||||
When configuring you only need to set values that you want to be different from the defaults.
|
||||
|
||||
__All config options and defaults are documented in the API docs__
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : { // core options go here
|
||||
"multiple" : false, // no multiselection
|
||||
"themes" : {
|
||||
"dots" : false // no connecting dots between dots
|
||||
}
|
||||
},
|
||||
"plugins" : ["state"] // activate the state plugin on this instance
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4485/)
|
||||
|
||||
We will cover all plugins further down.
|
||||
|
||||
__Keep in mind by default all modifications to the structure are prevented - that means drag'n'drop, create, rename, delete will not work unless you enable them.__
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : {
|
||||
"check_callback" : true, // enable all modifications
|
||||
},
|
||||
"plugins" : ["dnd","contextmenu"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4486/)
|
||||
|
||||
`"core.check_callback"` can also be set to a function, that will be invoked every time a modification is about to happen (or when jstree needs to check if a modification is possible). If you return `true` the operation will be allowed, a value of `false` means it will not be allowed. The possible operation you can expect are `create_node`, `rename_node`, `delete_node`, `move_node` and `copy_node`. The `more` parameter will contain various information provided by the plugin that is invoking the check. For example the DND plugin will provide an object containing information about the move or copy operation that is being checked - is it a multi tree operation, which node is currently hovered, where the insert arrow is pointing - before, after or inside, etc.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : {
|
||||
"check_callback" : function (operation, node, parent, position, more) {
|
||||
if(operation === "copy_node" || operation === "move_node") {
|
||||
if(parent.id === "#") {
|
||||
return false; // prevent moving a child above or below the root
|
||||
}
|
||||
},
|
||||
return true; // allow everything else
|
||||
}
|
||||
},
|
||||
"plugins" : ["dnd","contextmenu"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4487/)
|
||||
|
||||
The `more` parameter you receive contains other information related to the check being performed.
|
||||
|
||||
__For example__: `move_node` & `copy_node` checks will fire repeatedly while the user drags a node, if the check was triggered by the `dnd` plugin `more` will contain a `dnd` key, which will be set to `true`.
|
||||
You can check for `more.dnd` and only perform a certain action if `dnd` triggered the check.
|
||||
If you only want to perform an operation when a node is really about to be dropped check for `more.core`.
|
||||
|
||||
## Plugins
|
||||
|
||||
jsTree comes with a few plugin bundled, but they will only modify your tree if you activate them using the `"plugins"` config option. Here is a brief description of each plugin. You can read more on the available config options for each plugin in the API docs.
|
||||
|
||||
### checkbox
|
||||
Renders a checkbox icon in front of each node, making multiselection easy. It also has a "tri-state" option, meaning a node with some of its children checked will get a "square" icon.
|
||||
|
||||
_Keep in mind that if any sort of cascade is enabled, disabled nodes may be checked too (not by themselves, but for example when a parent of a disabled node is checked and selection is configured to cascade down)._
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"plugins" : ["checkbox"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4488/)
|
||||
|
||||
### contextmenu
|
||||
Makes it possible to right click nodes and shows a list of configurable actions in a menu.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : { "check_callback" : true }, // so that modifying operations work
|
||||
"plugins" : ["contextmenu"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4489/)
|
||||
|
||||
### dnd
|
||||
Makes it possible to drag and drop tree nodes and rearrange the tree.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : { "check_callback" : true }, // so that operations work
|
||||
"plugins" : ["dnd"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4490/)
|
||||
|
||||
### massload
|
||||
Makes it possible to load multiple nodes in a single go (for a lazy loaded tree).
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"core" : {
|
||||
"data" : { .. AJAX config .. }
|
||||
},
|
||||
"massload" : {
|
||||
"url" : "/some/path",
|
||||
"data" : function (nodes) {
|
||||
return { "ids" : nodes.join(",") };
|
||||
}
|
||||
},
|
||||
"plugins" : [ "massload", "state" ]
|
||||
});
|
||||
```
|
||||
|
||||
### search
|
||||
Adds the possibility to search for items in the tree and show only matching nodes. It also has AJAX / callback hooks, so that search will work on lazy loaded trees too.
|
||||
|
||||
```html
|
||||
<form id="s">
|
||||
<input type="search" id="q" />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<script>
|
||||
$("#container").jstree({
|
||||
"plugins" : ["search"]
|
||||
});
|
||||
$("#s").submit(function(e) {
|
||||
e.preventDefault();
|
||||
$("#container").jstree(true).search($("#q").val());
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4491/)
|
||||
|
||||
### sort
|
||||
Automatically arranges all sibling nodes according to a comparison config option function, which defaults to alphabetical order.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"plugins" : ["sort"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4492/)
|
||||
|
||||
### state
|
||||
Saves all opened and selected nodes in the user's browser, so when returning to the same tree the previous state will be restored.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
// the key is important if you have multiple trees in the same domain
|
||||
"state" : { "key" : "state_demo" },
|
||||
"plugins" : ["state"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4493/)
|
||||
|
||||
### types
|
||||
Makes it possible to add a "type" for a node, which means to easily control nesting rules and icon for groups of nodes instead of individually. To set a node type add a type property to the node structure.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"types" : {
|
||||
"default" : {
|
||||
"icon" : "glyphicon glyphicon-flash"
|
||||
},
|
||||
"demo" : {
|
||||
"icon" : "glyphicon glyphicon-ok"
|
||||
}
|
||||
},
|
||||
"plugins" : ["types"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4494/)
|
||||
|
||||
### unique
|
||||
Enforces that no nodes with the same name can coexist as siblings - prevents renaming and moving nodes to a parent, which already contains a node with the same name.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"plugins" : ["unique"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4495/)
|
||||
|
||||
### wholerow
|
||||
Makes each node appear block level which makes selection easier. May cause slow down for large trees in old browsers.
|
||||
|
||||
```js
|
||||
$("#tree").jstree({
|
||||
"plugins" : ["wholerow"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4496/)
|
||||
|
||||
### More plugins
|
||||
If you create your own plugin (or download a 3rd party one) you must include its source on the page and list its name in the `"plugins"` config array.
|
||||
|
||||
```js
|
||||
// conditional select
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.defaults.conditionalselect = function () { return true; };
|
||||
$.jstree.plugins.conditionalselect = function (options, parent) {
|
||||
this.activate_node = function (obj, e) {
|
||||
if(this.settings.conditionalselect.call(this, this.get_node(obj))) {
|
||||
parent.activate_node.call(this, obj, e);
|
||||
}
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
$("#tree").jstree({
|
||||
"conditionalselect" : function (node) {
|
||||
return node.text === "Root node" ? false : true;
|
||||
},
|
||||
"plugins" : ["conditionalselect"]
|
||||
});
|
||||
```
|
||||
|
||||
[view result](http://jsfiddle.net/vakata/2kwkh2uL/4497/)
|
||||
|
||||
As seen here when creating a plugin you can define a default config, add your own functions to jstree, or override existing ones while maintaining the ability to call the overridden function.
|
||||
|
||||
## PHP demos moved to new repository
|
||||
https://github.com/vakata/jstree-php-demos
|
||||
|
||||
## License & Contributing
|
||||
|
||||
_Please do NOT edit files in the "dist" subdirectory as they are generated via grunt. You'll find source code in the "src" subdirectory!_
|
||||
|
||||
If you want to you can always [donate a small amount][paypal] to help the development of jstree.
|
||||
|
||||
[paypal]: https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=paypal@vakata.com¤cy_code=USD&amount=&return=http://jstree.com/donation&item_name=Buy+me+a+coffee+for+jsTree
|
||||
|
||||
Copyright (c) 2014 Ivan Bozhanov (http://vakata.com)
|
||||
|
||||
Licensed under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
|
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "jstree",
|
||||
"license": "MIT",
|
||||
"version": "3.3.7",
|
||||
"main" : [
|
||||
"./dist/jstree.js",
|
||||
"./dist/themes/default/style.css"
|
||||
],
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"docs",
|
||||
"demo",
|
||||
"libs",
|
||||
"node_modules",
|
||||
"test",
|
||||
"libs",
|
||||
"jstree.jquery.json",
|
||||
"gruntfile.js",
|
||||
"package.json",
|
||||
"bower.json",
|
||||
"component.json",
|
||||
"LICENCE-MIT",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.9.1"
|
||||
},
|
||||
"keywords": [
|
||||
"ui",
|
||||
"tree",
|
||||
"jstree"
|
||||
]
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "jstree",
|
||||
"repo": "vakata/jstree",
|
||||
"description": "jsTree is jquery plugin, that provides interactive trees.",
|
||||
"version": "3.3.7",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"ui",
|
||||
"tree",
|
||||
"jstree"
|
||||
],
|
||||
"scripts": [
|
||||
"dist/jstree.js",
|
||||
"dist/jstree.min.js"
|
||||
],
|
||||
"images": [
|
||||
"dist/themes/default/32px.png",
|
||||
"dist/themes/default/40px.png",
|
||||
"dist/themes/default/throbber.gif"
|
||||
],
|
||||
"styles": [
|
||||
"dist/themes/default/style.css",
|
||||
"dist/themes/default/style.min.css"
|
||||
],
|
||||
"dependencies": {
|
||||
"components/jquery": ">=1.9.1"
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "vakata/jstree",
|
||||
"description": "jsTree is jquery plugin, that provides interactive trees.",
|
||||
"type": "component",
|
||||
"homepage": "http://jstree.com",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/vakata/jstree/issues",
|
||||
"forum": "https://groups.google.com/forum/#!forum/jstree",
|
||||
"source": "https://github.com/vakata/jstree"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ivan Bozhanov",
|
||||
"email": "jstree@jstree.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"components/jquery": ">=1.9.1"
|
||||
},
|
||||
"suggest": {
|
||||
"robloach/component-installer": "Allows installation of Components via Composer"
|
||||
},
|
||||
"extra": {
|
||||
"component": {
|
||||
"scripts": [
|
||||
"dist/jstree.js"
|
||||
],
|
||||
"styles": [
|
||||
"dist/themes/default/style.css"
|
||||
],
|
||||
"images": [
|
||||
"dist/themes/default/32px.png",
|
||||
"dist/themes/default/40px.png",
|
||||
"dist/themes/default/throbber.gif"
|
||||
],
|
||||
"files": [
|
||||
"dist/jstree.min.js",
|
||||
"dist/themes/default/style.min.css",
|
||||
"dist/themes/default/32px.png",
|
||||
"dist/themes/default/40px.png",
|
||||
"dist/themes/default/throbber.gif"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
## PHP demos moved to new repository
|
||||
https://github.com/vakata/jstree-php-demos
|
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>jstree basic demos</title>
|
||||
<style>
|
||||
html { margin:0; padding:0; font-size:62.5%; }
|
||||
body { max-width:800px; min-width:300px; margin:0 auto; padding:20px 10px; font-size:14px; font-size:1.4em; }
|
||||
h1 { font-size:1.8em; }
|
||||
.demo { overflow:auto; border:1px solid silver; min-height:100px; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="./../../dist/themes/default/style.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTML demo</h1>
|
||||
<div id="html" class="demo">
|
||||
<ul>
|
||||
<li data-jstree='{ "opened" : true }'>Root node
|
||||
<ul>
|
||||
<li data-jstree='{ "selected" : true }'>Child node 1</li>
|
||||
<li>Child node 2</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1>Inline data demo</h1>
|
||||
<div id="data" class="demo"></div>
|
||||
|
||||
<h1>Data format demo</h1>
|
||||
<div id="frmt" class="demo"></div>
|
||||
|
||||
<h1>AJAX demo</h1>
|
||||
<div id="ajax" class="demo"></div>
|
||||
|
||||
<h1>Lazy loading demo</h1>
|
||||
<div id="lazy" class="demo"></div>
|
||||
|
||||
<h1>Callback function data demo</h1>
|
||||
<div id="clbk" class="demo"></div>
|
||||
|
||||
<h1>Interaction and events demo</h1>
|
||||
<button id="evts_button">select node with id 1</button> <em>either click the button or a node in the tree</em>
|
||||
<div id="evts" class="demo"></div>
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
|
||||
<script src="./../../dist/jstree.min.js"></script>
|
||||
|
||||
<script>
|
||||
// html demo
|
||||
$('#html').jstree();
|
||||
|
||||
// inline data demo
|
||||
$('#data').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{ "text" : "Root node", "children" : [
|
||||
{ "text" : "Child node 1" },
|
||||
{ "text" : "Child node 2" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// data format demo
|
||||
$('#frmt').jstree({
|
||||
'core' : {
|
||||
'data' : [
|
||||
{
|
||||
"text" : "Root node",
|
||||
"state" : { "opened" : true },
|
||||
"children" : [
|
||||
{
|
||||
"text" : "Child node 1",
|
||||
"state" : { "selected" : true },
|
||||
"icon" : "jstree-file"
|
||||
},
|
||||
{ "text" : "Child node 2", "state" : { "disabled" : true } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// ajax demo
|
||||
$('#ajax').jstree({
|
||||
'core' : {
|
||||
'data' : {
|
||||
"url" : "./root.json",
|
||||
"dataType" : "json" // needed only if you do not supply JSON headers
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// lazy demo
|
||||
$('#lazy').jstree({
|
||||
'core' : {
|
||||
'data' : {
|
||||
"url" : "//www.jstree.com/fiddle/?lazy",
|
||||
"data" : function (node) {
|
||||
return { "id" : node.id };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// data from callback
|
||||
$('#clbk').jstree({
|
||||
'core' : {
|
||||
'data' : function (node, cb) {
|
||||
if(node.id === "#") {
|
||||
cb([{"text" : "Root", "id" : "1", "children" : true}]);
|
||||
}
|
||||
else {
|
||||
cb(["Child"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// interaction and events
|
||||
$('#evts_button').on("click", function () {
|
||||
var instance = $('#evts').jstree(true);
|
||||
instance.deselect_all();
|
||||
instance.select_node('1');
|
||||
});
|
||||
$('#evts')
|
||||
.on("changed.jstree", function (e, data) {
|
||||
if(data.selected.length) {
|
||||
alert('The selected node is: ' + data.instance.get_node(data.selected[0]).text);
|
||||
}
|
||||
})
|
||||
.jstree({
|
||||
'core' : {
|
||||
'multiple' : false,
|
||||
'data' : [
|
||||
{ "text" : "Root node", "children" : [
|
||||
{ "text" : "Child node 1", "id" : 1 },
|
||||
{ "text" : "Child node 2" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1 @@
|
||||
[{"id":1,"text":"Root node","children":[{"id":2,"text":"Child node 1"},{"id":3,"text":"Child node 2"}]}]
|
8605
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/jstree.js
vendored
Normal file
6
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/jstree.min.js
vendored
Normal file
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/32px.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/40px.png
vendored
Normal file
After Width: | Height: | Size: 6.4 KiB |
1146
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/style.css
vendored
Normal file
1
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/style.min.css
vendored
Normal file
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/throbber.gif
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/32px.png
vendored
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/40px.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
1102
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/style.css
vendored
Normal file
1
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/style.min.css
vendored
Normal file
BIN
app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/throbber.gif
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
242
app/YtManagerApp/static/YtManagerApp/import/jstree/gruntfile.js
Normal file
@ -0,0 +1,242 @@
|
||||
/*global module:false, require:false, __dirname:false*/
|
||||
var _ = require('lodash');
|
||||
|
||||
module.exports = function(grunt) {
|
||||
grunt.util.linefeed = "\n";
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
concat: {
|
||||
options : {
|
||||
separator : "\n"
|
||||
},
|
||||
dist: {
|
||||
src: ['src/<%= pkg.name %>.js', 'src/<%= pkg.name %>.*.js', 'src/vakata-jstree.js'],
|
||||
dest: 'dist/<%= pkg.name %>.js'
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
libs : {
|
||||
files : [
|
||||
{ expand: true, cwd : 'libs/', src: ['*'], dest: 'dist/libs/' }
|
||||
]
|
||||
},
|
||||
docs : {
|
||||
files : [
|
||||
{ expand: true, cwd : 'dist/', src: ['**/*'], dest: 'docs/assets/dist/' }
|
||||
]
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> - (<%= _.map(pkg.licenses, "type").join(", ") %>) */\n',
|
||||
preserveComments: false,
|
||||
//sourceMap: "dist/jstree.min.map",
|
||||
//sourceMappingURL: "jstree.min.map",
|
||||
report: "min",
|
||||
output: {
|
||||
ascii_only: true
|
||||
},
|
||||
compress: {
|
||||
hoist_funs: false,
|
||||
loops: false,
|
||||
unused: false
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
src: ['<%= concat.dist.dest %>'],
|
||||
dest: 'dist/<%= pkg.name %>.min.js'
|
||||
}
|
||||
},
|
||||
qunit: {
|
||||
files: ['test/unit/**/*.html']
|
||||
},
|
||||
jshint: {
|
||||
options: {
|
||||
'curly' : true,
|
||||
'eqeqeq' : true,
|
||||
'latedef' : true,
|
||||
'newcap' : true,
|
||||
'noarg' : true,
|
||||
'sub' : true,
|
||||
'undef' : true,
|
||||
'boss' : true,
|
||||
'eqnull' : true,
|
||||
'browser' : true,
|
||||
'trailing' : true,
|
||||
'globals' : {
|
||||
'console' : true,
|
||||
'jQuery' : true,
|
||||
'browser' : true,
|
||||
'XSLTProcessor' : true,
|
||||
'ActiveXObject' : true
|
||||
}
|
||||
},
|
||||
beforeconcat: ['src/<%= pkg.name %>.js', 'src/<%= pkg.name %>.*.js'],
|
||||
afterconcat: ['dist/<%= pkg.name %>.js']
|
||||
},
|
||||
dox: {
|
||||
files: {
|
||||
src: ['src/*.js'],
|
||||
dest: 'docs'
|
||||
}
|
||||
},
|
||||
amd : {
|
||||
files: {
|
||||
src: ['dist/jstree.js'],
|
||||
dest: 'dist/jstree.js'
|
||||
}
|
||||
},
|
||||
less: {
|
||||
production: {
|
||||
options : {
|
||||
cleancss : true,
|
||||
compress : true
|
||||
},
|
||||
files: {
|
||||
"dist/themes/default/style.min.css" : "src/themes/default/style.less",
|
||||
"dist/themes/default-dark/style.min.css" : "src/themes/default-dark/style.less"
|
||||
}
|
||||
},
|
||||
development: {
|
||||
files: {
|
||||
"src/themes/default/style.css" : "src/themes/default/style.less",
|
||||
"dist/themes/default/style.css" : "src/themes/default/style.less",
|
||||
"src/themes/default-dark/style.css" : "src/themes/default-dark/style.less",
|
||||
"dist/themes/default-dark/style.css" : "src/themes/default-dark/style.less"
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
js : {
|
||||
files: ['src/**/*.js'],
|
||||
tasks: ['js'],
|
||||
options : {
|
||||
atBegin : true
|
||||
}
|
||||
},
|
||||
css : {
|
||||
files: ['src/**/*.less','src/**/*.png','src/**/*.gif'],
|
||||
tasks: ['css'],
|
||||
options : {
|
||||
atBegin : true
|
||||
}
|
||||
},
|
||||
},
|
||||
resemble: {
|
||||
options: {
|
||||
screenshotRoot: 'test/visual/screenshots/',
|
||||
url: 'http://127.0.0.1/jstree/test/visual/',
|
||||
gm: false
|
||||
},
|
||||
desktop: {
|
||||
options: {
|
||||
width: 1280,
|
||||
},
|
||||
src: ['desktop'],
|
||||
dest: 'desktop',
|
||||
},
|
||||
mobile: {
|
||||
options: {
|
||||
width: 360,
|
||||
},
|
||||
src: ['mobile'],
|
||||
dest: 'mobile'
|
||||
}
|
||||
},
|
||||
imagemin: {
|
||||
dynamic: {
|
||||
options: { // Target options
|
||||
optimizationLevel: 7,
|
||||
pngquant : true
|
||||
},
|
||||
files: [{
|
||||
expand: true, // Enable dynamic expansion
|
||||
cwd: 'src/themes/default/', // Src matches are relative to this path
|
||||
src: ['**/*.{png,jpg,gif}'], // Actual patterns to match
|
||||
dest: 'dist/themes/default/' // Destination path prefix
|
||||
},{
|
||||
expand: true, // Enable dynamic expansion
|
||||
cwd: 'src/themes/default-dark/', // Src matches are relative to this path
|
||||
src: ['**/*.{png,jpg,gif}'], // Actual patterns to match
|
||||
dest: 'dist/themes/default-dark/' // Destination path prefix
|
||||
}]
|
||||
}
|
||||
},
|
||||
replace: {
|
||||
files: {
|
||||
src: ['dist/*.js', 'bower.json', 'component.json', 'jstree.jquery.json'],
|
||||
overwrite: true,
|
||||
replacements: [
|
||||
{
|
||||
from: '{{VERSION}}',
|
||||
to: "<%= pkg.version %>"
|
||||
},
|
||||
{
|
||||
from: /"version": "[^"]+"/g,
|
||||
to: "\"version\": \"<%= pkg.version %>\""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-qunit');
|
||||
grunt.loadNpmTasks('grunt-resemble-cli');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-contrib-imagemin');
|
||||
grunt.loadNpmTasks('grunt-text-replace');
|
||||
|
||||
grunt.registerMultiTask('amd', 'Clean up AMD', function () {
|
||||
var s, d;
|
||||
this.files.forEach(function (f) {
|
||||
s = f.src[0];
|
||||
d = f.dest;
|
||||
});
|
||||
grunt.file.copy(s, d, {
|
||||
process: function (contents) {
|
||||
contents = contents.replace(/\s*if\(\$\.jstree\.plugins\.[a-z]+\)\s*\{\s*return;\s*\}/ig, '');
|
||||
contents = contents.replace(/\/\*globals[^\/]+\//ig, '');
|
||||
//contents = contents.replace(/\(function \(factory[\s\S]*?undefined/mig, '(function ($, undefined');
|
||||
//contents = contents.replace(/\}\)\);/g, '}(jQuery));');
|
||||
contents = contents.replace(/\(function \(factory[\s\S]*?undefined\s*\)[^\n]+/mig, '');
|
||||
contents = contents.replace(/\}\)\);/g, '');
|
||||
contents = contents.replace(/\s*("|')use strict("|');/g, '');
|
||||
contents = contents.replace(/\s*return \$\.fn\.jstree;/g, '');
|
||||
return grunt.file.read('src/intro.js') + contents + grunt.file.read('src/outro.js');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
grunt.registerMultiTask('dox', 'Generate dox output ', function() {
|
||||
var exec = require('child_process').exec,
|
||||
path = require('path'),
|
||||
done = this.async(),
|
||||
doxPath = path.resolve(__dirname),
|
||||
formatter = [doxPath, 'node_modules', '.bin', 'dox'].join(path.sep);
|
||||
exec(formatter + ' < "dist/jstree.js" > "docs/jstree.json"', {maxBuffer: 5000*1024}, function(error, stout, sterr){
|
||||
if (error) {
|
||||
grunt.log.error(formatter);
|
||||
grunt.log.error("WARN: "+ error);
|
||||
}
|
||||
if (!error) {
|
||||
grunt.log.writeln('dist/jstree.js doxxed.');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
grunt.util.linefeed = "\n";
|
||||
|
||||
// Default task.
|
||||
grunt.registerTask('default', ['jshint:beforeconcat','concat','amd','jshint:afterconcat','copy:libs','uglify','less','imagemin','replace','copy:docs','qunit','resemble','dox']);
|
||||
grunt.registerTask('js', ['concat','amd','uglify']);
|
||||
grunt.registerTask('css', ['copy','less']);
|
||||
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "jstree",
|
||||
"title": "jsTree",
|
||||
"description": "Tree view for jQuery",
|
||||
"version": "3.3.7",
|
||||
"homepage": "http://jstree.com",
|
||||
"keywords": [
|
||||
"ui",
|
||||
"tree",
|
||||
"jstree"
|
||||
],
|
||||
"author": {
|
||||
"name": "Ivan Bozhanov",
|
||||
"email": "jstree@jstree.com",
|
||||
"url": "http://vakata.com"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/vakata/jstree/blob/master/LICENSE-MIT"
|
||||
}
|
||||
],
|
||||
"bugs": "https://github.com/vakata/jstree/issues",
|
||||
"demo": "http://jstree.com/demo",
|
||||
"dependencies": {
|
||||
"jquery": ">=1.9.1"
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "jstree",
|
||||
"title": "jsTree",
|
||||
"description": "jQuery tree plugin",
|
||||
"version": "3.3.7",
|
||||
"homepage": "http://jstree.com",
|
||||
"main": "./dist/jstree.js",
|
||||
"author": {
|
||||
"name": "Ivan Bozhanov",
|
||||
"email": "jstree@jstree.com",
|
||||
"url": "http://vakata.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/vakata/jstree.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vakata/jstree/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/vakata/jstree/blob/master/LICENSE-MIT"
|
||||
}
|
||||
],
|
||||
"keywords": [],
|
||||
"devDependencies": {
|
||||
"dox": "~0.9.0",
|
||||
"grunt": "~1.0.0",
|
||||
"grunt-contrib-concat": "*",
|
||||
"grunt-contrib-copy": "*",
|
||||
"grunt-contrib-imagemin": "~2.0.1",
|
||||
"grunt-contrib-jshint": "*",
|
||||
"grunt-contrib-less": "~1.4.1",
|
||||
"grunt-contrib-qunit": "~2.0.0",
|
||||
"grunt-contrib-uglify": "*",
|
||||
"grunt-contrib-watch": "~1.1.0",
|
||||
"grunt-resemble-cli": "0.0.8",
|
||||
"grunt-text-replace": "~0.4.0",
|
||||
"lodash": "^4.17.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": ">=1.9.1"
|
||||
},
|
||||
"npmName": "jstree",
|
||||
"npmFileMap": [
|
||||
{
|
||||
"basePath": "/dist/",
|
||||
"files": [
|
||||
"jstree.min.js",
|
||||
"themes/**/*.png",
|
||||
"themes/**/*.gif",
|
||||
"themes/**/*.min.css"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
/*globals jQuery, define, module, exports, require, window, document, postMessage */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['jquery'], factory);
|
||||
}
|
||||
else if(typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = factory(require('jquery'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($, undefined) {
|
||||
"use strict";
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* ### Changed plugin
|
||||
*
|
||||
* This plugin adds more information to the `changed.jstree` event. The new data is contained in the `changed` event data property, and contains a lists of `selected` and `deselected` nodes.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.changed', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.changed) { return; }
|
||||
|
||||
$.jstree.plugins.changed = function (options, parent) {
|
||||
var last = [];
|
||||
this.trigger = function (ev, data) {
|
||||
var i, j;
|
||||
if(!data) {
|
||||
data = {};
|
||||
}
|
||||
if(ev.replace('.jstree','') === 'changed') {
|
||||
data.changed = { selected : [], deselected : [] };
|
||||
var tmp = {};
|
||||
for(i = 0, j = last.length; i < j; i++) {
|
||||
tmp[last[i]] = 1;
|
||||
}
|
||||
for(i = 0, j = data.selected.length; i < j; i++) {
|
||||
if(!tmp[data.selected[i]]) {
|
||||
data.changed.selected.push(data.selected[i]);
|
||||
}
|
||||
else {
|
||||
tmp[data.selected[i]] = 2;
|
||||
}
|
||||
}
|
||||
for(i = 0, j = last.length; i < j; i++) {
|
||||
if(tmp[last[i]] === 1) {
|
||||
data.changed.deselected.push(last[i]);
|
||||
}
|
||||
}
|
||||
last = data.selected.slice();
|
||||
}
|
||||
/**
|
||||
* triggered when selection changes (the "changed" plugin enhances the original event with more data)
|
||||
* @event
|
||||
* @name changed.jstree
|
||||
* @param {Object} node
|
||||
* @param {Object} action the action that caused the selection to change
|
||||
* @param {Array} selected the current selection
|
||||
* @param {Object} changed an object containing two properties `selected` and `deselected` - both arrays of node IDs, which were selected or deselected since the last changed event
|
||||
* @param {Object} event the event (if any) that triggered this changed event
|
||||
* @plugin changed
|
||||
*/
|
||||
parent.trigger.call(this, ev, data);
|
||||
};
|
||||
this.refresh = function (skip_loading, forget_state) {
|
||||
last = [];
|
||||
return parent.refresh.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
}));
|
@ -0,0 +1,976 @@
|
||||
/**
|
||||
* ### Checkbox plugin
|
||||
*
|
||||
* This plugin renders checkbox icons in front of each node, making multiple selection much easier.
|
||||
* It also supports tri-state behavior, meaning that if a node has a few of its children checked it will be rendered as undetermined, and state will be propagated up.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.checkbox', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.checkbox) { return; }
|
||||
|
||||
var _i = document.createElement('I');
|
||||
_i.className = 'jstree-icon jstree-checkbox';
|
||||
_i.setAttribute('role', 'presentation');
|
||||
/**
|
||||
* stores all defaults for the checkbox plugin
|
||||
* @name $.jstree.defaults.checkbox
|
||||
* @plugin checkbox
|
||||
*/
|
||||
$.jstree.defaults.checkbox = {
|
||||
/**
|
||||
* a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`.
|
||||
* @name $.jstree.defaults.checkbox.visible
|
||||
* @plugin checkbox
|
||||
*/
|
||||
visible : true,
|
||||
/**
|
||||
* a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`.
|
||||
* @name $.jstree.defaults.checkbox.three_state
|
||||
* @plugin checkbox
|
||||
*/
|
||||
three_state : true,
|
||||
/**
|
||||
* a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`.
|
||||
* @name $.jstree.defaults.checkbox.whole_node
|
||||
* @plugin checkbox
|
||||
*/
|
||||
whole_node : true,
|
||||
/**
|
||||
* a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`.
|
||||
* @name $.jstree.defaults.checkbox.keep_selected_style
|
||||
* @plugin checkbox
|
||||
*/
|
||||
keep_selected_style : true,
|
||||
/**
|
||||
* This setting controls how cascading and undetermined nodes are applied.
|
||||
* If 'up' is in the string - cascading up is enabled, if 'down' is in the string - cascading down is enabled, if 'undetermined' is in the string - undetermined nodes will be used.
|
||||
* If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''.
|
||||
* @name $.jstree.defaults.checkbox.cascade
|
||||
* @plugin checkbox
|
||||
*/
|
||||
cascade : '',
|
||||
/**
|
||||
* This setting controls if checkbox are bound to the general tree selection or to an internal array maintained by the checkbox plugin. Defaults to `true`, only set to `false` if you know exactly what you are doing.
|
||||
* @name $.jstree.defaults.checkbox.tie_selection
|
||||
* @plugin checkbox
|
||||
*/
|
||||
tie_selection : true,
|
||||
|
||||
/**
|
||||
* This setting controls if cascading down affects disabled checkboxes
|
||||
* @name $.jstree.defaults.checkbox.cascade_to_disabled
|
||||
* @plugin checkbox
|
||||
*/
|
||||
cascade_to_disabled : true,
|
||||
|
||||
/**
|
||||
* This setting controls if cascading down affects hidden checkboxes
|
||||
* @name $.jstree.defaults.checkbox.cascade_to_hidden
|
||||
* @plugin checkbox
|
||||
*/
|
||||
cascade_to_hidden : true
|
||||
};
|
||||
$.jstree.plugins.checkbox = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
this._data.checkbox.uto = false;
|
||||
this._data.checkbox.selected = [];
|
||||
if(this.settings.checkbox.three_state) {
|
||||
this.settings.checkbox.cascade = 'up+down+undetermined';
|
||||
}
|
||||
this.element
|
||||
.on("init.jstree", $.proxy(function () {
|
||||
this._data.checkbox.visible = this.settings.checkbox.visible;
|
||||
if(!this.settings.checkbox.keep_selected_style) {
|
||||
this.element.addClass('jstree-checkbox-no-clicked');
|
||||
}
|
||||
if(this.settings.checkbox.tie_selection) {
|
||||
this.element.addClass('jstree-checkbox-selection');
|
||||
}
|
||||
}, this))
|
||||
.on("loading.jstree", $.proxy(function () {
|
||||
this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ]();
|
||||
}, this));
|
||||
if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
|
||||
this.element
|
||||
.on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree', $.proxy(function () {
|
||||
// only if undetermined is in setting
|
||||
if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
|
||||
this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
|
||||
}, this));
|
||||
}
|
||||
if(!this.settings.checkbox.tie_selection) {
|
||||
this.element
|
||||
.on('model.jstree', $.proxy(function (e, data) {
|
||||
var m = this._model.data,
|
||||
p = m[data.parent],
|
||||
dpc = data.nodes,
|
||||
i, j;
|
||||
for(i = 0, j = dpc.length; i < j; i++) {
|
||||
m[dpc[i]].state.checked = m[dpc[i]].state.checked || (m[dpc[i]].original && m[dpc[i]].original.state && m[dpc[i]].original.state.checked);
|
||||
if(m[dpc[i]].state.checked) {
|
||||
this._data.checkbox.selected.push(dpc[i]);
|
||||
}
|
||||
}
|
||||
}, this));
|
||||
}
|
||||
if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) {
|
||||
this.element
|
||||
.on('model.jstree', $.proxy(function (e, data) {
|
||||
var m = this._model.data,
|
||||
p = m[data.parent],
|
||||
dpc = data.nodes,
|
||||
chd = [],
|
||||
c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
|
||||
|
||||
if(s.indexOf('down') !== -1) {
|
||||
// apply down
|
||||
if(p.state[ t ? 'selected' : 'checked' ]) {
|
||||
for(i = 0, j = dpc.length; i < j; i++) {
|
||||
m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true;
|
||||
}
|
||||
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc);
|
||||
}
|
||||
else {
|
||||
for(i = 0, j = dpc.length; i < j; i++) {
|
||||
if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) {
|
||||
for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) {
|
||||
m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true;
|
||||
}
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(s.indexOf('up') !== -1) {
|
||||
// apply up
|
||||
for(i = 0, j = p.children_d.length; i < j; i++) {
|
||||
if(!m[p.children_d[i]].children.length) {
|
||||
chd.push(m[p.children_d[i]].parent);
|
||||
}
|
||||
}
|
||||
chd = $.vakata.array_unique(chd);
|
||||
for(k = 0, l = chd.length; k < l; k++) {
|
||||
p = m[chd[k]];
|
||||
while(p && p.id !== $.jstree.root) {
|
||||
c = 0;
|
||||
for(i = 0, j = p.children.length; i < j; i++) {
|
||||
c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
|
||||
}
|
||||
if(c === j) {
|
||||
p.state[ t ? 'selected' : 'checked' ] = true;
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
|
||||
tmp = this.get_node(p, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
p = this.get_node(p.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected);
|
||||
}, this))
|
||||
.on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) {
|
||||
var self = this,
|
||||
obj = data.node,
|
||||
m = this._model.data,
|
||||
par = this.get_node(obj.parent),
|
||||
i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection,
|
||||
sel = {}, cur = this._data[ t ? 'core' : 'checkbox' ].selected;
|
||||
|
||||
for (i = 0, j = cur.length; i < j; i++) {
|
||||
sel[cur[i]] = true;
|
||||
}
|
||||
|
||||
// apply down
|
||||
if(s.indexOf('down') !== -1) {
|
||||
//this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d));
|
||||
var selectedIds = this._cascade_new_checked_state(obj.id, true);
|
||||
var temp = obj.children_d.concat(obj.id);
|
||||
for (i = 0, j = temp.length; i < j; i++) {
|
||||
if (selectedIds.indexOf(temp[i]) > -1) {
|
||||
sel[temp[i]] = true;
|
||||
}
|
||||
else {
|
||||
delete sel[temp[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply up
|
||||
if(s.indexOf('up') !== -1) {
|
||||
while(par && par.id !== $.jstree.root) {
|
||||
c = 0;
|
||||
for(i = 0, j = par.children.length; i < j; i++) {
|
||||
c += m[par.children[i]].state[ t ? 'selected' : 'checked' ];
|
||||
}
|
||||
if(c === j) {
|
||||
par.state[ t ? 'selected' : 'checked' ] = true;
|
||||
sel[par.id] = true;
|
||||
//this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id);
|
||||
tmp = this.get_node(par, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
par = this.get_node(par.parent);
|
||||
}
|
||||
}
|
||||
|
||||
cur = [];
|
||||
for (i in sel) {
|
||||
if (sel.hasOwnProperty(i)) {
|
||||
cur.push(i);
|
||||
}
|
||||
}
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = cur;
|
||||
}, this))
|
||||
.on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) {
|
||||
var obj = this.get_node($.jstree.root),
|
||||
m = this._model.data,
|
||||
i, j, tmp;
|
||||
for(i = 0, j = obj.children_d.length; i < j; i++) {
|
||||
tmp = m[obj.children_d[i]];
|
||||
if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
|
||||
tmp.original.state.undetermined = false;
|
||||
}
|
||||
}
|
||||
}, this))
|
||||
.on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) {
|
||||
var self = this,
|
||||
obj = data.node,
|
||||
dom = this.get_node(obj, true),
|
||||
i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection,
|
||||
cur = this._data[ t ? 'core' : 'checkbox' ].selected, sel = {},
|
||||
stillSelectedIds = [],
|
||||
allIds = obj.children_d.concat(obj.id);
|
||||
|
||||
// apply down
|
||||
if(s.indexOf('down') !== -1) {
|
||||
var selectedIds = this._cascade_new_checked_state(obj.id, false);
|
||||
|
||||
cur = cur.filter(function(id) {
|
||||
return allIds.indexOf(id) === -1 || selectedIds.indexOf(id) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
// only apply up if cascade up is enabled and if this node is not selected
|
||||
// (if all child nodes are disabled and cascade_to_disabled === false then this node will till be selected).
|
||||
if(s.indexOf('up') !== -1 && cur.indexOf(obj.id) === -1) {
|
||||
for(i = 0, j = obj.parents.length; i < j; i++) {
|
||||
tmp = this._model.data[obj.parents[i]];
|
||||
tmp.state[ t ? 'selected' : 'checked' ] = false;
|
||||
if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
|
||||
tmp.original.state.undetermined = false;
|
||||
}
|
||||
tmp = this.get_node(obj.parents[i], true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
|
||||
cur = cur.filter(function(id) {
|
||||
return obj.parents.indexOf(id) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = cur;
|
||||
}, this));
|
||||
}
|
||||
if(this.settings.checkbox.cascade.indexOf('up') !== -1) {
|
||||
this.element
|
||||
.on('delete_node.jstree', $.proxy(function (e, data) {
|
||||
// apply up (whole handler)
|
||||
var p = this.get_node(data.parent),
|
||||
m = this._model.data,
|
||||
i, j, c, tmp, t = this.settings.checkbox.tie_selection;
|
||||
while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
|
||||
c = 0;
|
||||
for(i = 0, j = p.children.length; i < j; i++) {
|
||||
c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
|
||||
}
|
||||
if(j > 0 && c === j) {
|
||||
p.state[ t ? 'selected' : 'checked' ] = true;
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
|
||||
tmp = this.get_node(p, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
p = this.get_node(p.parent);
|
||||
}
|
||||
}, this))
|
||||
.on('move_node.jstree', $.proxy(function (e, data) {
|
||||
// apply up (whole handler)
|
||||
var is_multi = data.is_multi,
|
||||
old_par = data.old_parent,
|
||||
new_par = this.get_node(data.parent),
|
||||
m = this._model.data,
|
||||
p, c, i, j, tmp, t = this.settings.checkbox.tie_selection;
|
||||
if(!is_multi) {
|
||||
p = this.get_node(old_par);
|
||||
while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
|
||||
c = 0;
|
||||
for(i = 0, j = p.children.length; i < j; i++) {
|
||||
c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
|
||||
}
|
||||
if(j > 0 && c === j) {
|
||||
p.state[ t ? 'selected' : 'checked' ] = true;
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
|
||||
tmp = this.get_node(p, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
p = this.get_node(p.parent);
|
||||
}
|
||||
}
|
||||
p = new_par;
|
||||
while(p && p.id !== $.jstree.root) {
|
||||
c = 0;
|
||||
for(i = 0, j = p.children.length; i < j; i++) {
|
||||
c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
|
||||
}
|
||||
if(c === j) {
|
||||
if(!p.state[ t ? 'selected' : 'checked' ]) {
|
||||
p.state[ t ? 'selected' : 'checked' ] = true;
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
|
||||
tmp = this.get_node(p, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(p.state[ t ? 'selected' : 'checked' ]) {
|
||||
p.state[ t ? 'selected' : 'checked' ] = false;
|
||||
this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id);
|
||||
tmp = this.get_node(p, true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
p = this.get_node(p.parent);
|
||||
}
|
||||
}, this));
|
||||
}
|
||||
};
|
||||
/**
|
||||
* get an array of all nodes whose state is "undetermined"
|
||||
* @name get_undetermined([full])
|
||||
* @param {boolean} full: if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
|
||||
* @return {Array}
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.get_undetermined = function (full) {
|
||||
if (this.settings.checkbox.cascade.indexOf('undetermined') === -1) {
|
||||
return [];
|
||||
}
|
||||
var i, j, k, l, o = {}, m = this._model.data, t = this.settings.checkbox.tie_selection, s = this._data[ t ? 'core' : 'checkbox' ].selected, p = [], tt = this, r = [];
|
||||
for(i = 0, j = s.length; i < j; i++) {
|
||||
if(m[s[i]] && m[s[i]].parents) {
|
||||
for(k = 0, l = m[s[i]].parents.length; k < l; k++) {
|
||||
if(o[m[s[i]].parents[k]] !== undefined) {
|
||||
break;
|
||||
}
|
||||
if(m[s[i]].parents[k] !== $.jstree.root) {
|
||||
o[m[s[i]].parents[k]] = true;
|
||||
p.push(m[s[i]].parents[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// attempt for server side undetermined state
|
||||
this.element.find('.jstree-closed').not(':has(.jstree-children)')
|
||||
.each(function () {
|
||||
var tmp = tt.get_node(this), tmp2;
|
||||
|
||||
if(!tmp) { return; }
|
||||
|
||||
if(!tmp.state.loaded) {
|
||||
if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) {
|
||||
if(o[tmp.id] === undefined && tmp.id !== $.jstree.root) {
|
||||
o[tmp.id] = true;
|
||||
p.push(tmp.id);
|
||||
}
|
||||
for(k = 0, l = tmp.parents.length; k < l; k++) {
|
||||
if(o[tmp.parents[k]] === undefined && tmp.parents[k] !== $.jstree.root) {
|
||||
o[tmp.parents[k]] = true;
|
||||
p.push(tmp.parents[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(i = 0, j = tmp.children_d.length; i < j; i++) {
|
||||
tmp2 = m[tmp.children_d[i]];
|
||||
if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) {
|
||||
if(o[tmp2.id] === undefined && tmp2.id !== $.jstree.root) {
|
||||
o[tmp2.id] = true;
|
||||
p.push(tmp2.id);
|
||||
}
|
||||
for(k = 0, l = tmp2.parents.length; k < l; k++) {
|
||||
if(o[tmp2.parents[k]] === undefined && tmp2.parents[k] !== $.jstree.root) {
|
||||
o[tmp2.parents[k]] = true;
|
||||
p.push(tmp2.parents[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
for (i = 0, j = p.length; i < j; i++) {
|
||||
if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) {
|
||||
r.push(full ? m[p[i]] : p[i]);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
/**
|
||||
* set the undetermined state where and if necessary. Used internally.
|
||||
* @private
|
||||
* @name _undetermined()
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this._undetermined = function () {
|
||||
if(this.element === null) { return; }
|
||||
var p = this.get_undetermined(false), i, j, s;
|
||||
|
||||
this.element.find('.jstree-undetermined').removeClass('jstree-undetermined');
|
||||
for (i = 0, j = p.length; i < j; i++) {
|
||||
s = this.get_node(p[i], true);
|
||||
if(s && s.length) {
|
||||
s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined');
|
||||
}
|
||||
}
|
||||
};
|
||||
this.redraw_node = function(obj, deep, is_callback, force_render) {
|
||||
obj = parent.redraw_node.apply(this, arguments);
|
||||
if(obj) {
|
||||
var i, j, tmp = null, icon = null;
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; }
|
||||
icon = _i.cloneNode(false);
|
||||
if(this._model.data[obj.id].state.checkbox_disabled) { icon.className += ' jstree-checkbox-disabled'; }
|
||||
tmp.insertBefore(icon, tmp.childNodes[0]);
|
||||
}
|
||||
}
|
||||
if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
|
||||
if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
|
||||
this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
/**
|
||||
* show the node checkbox icons
|
||||
* @name show_checkboxes()
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); };
|
||||
/**
|
||||
* hide the node checkbox icons
|
||||
* @name hide_checkboxes()
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); };
|
||||
/**
|
||||
* toggle the node icons
|
||||
* @name toggle_checkboxes()
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } };
|
||||
/**
|
||||
* checks if a node is in an undetermined state
|
||||
* @name is_undetermined(obj)
|
||||
* @param {mixed} obj
|
||||
* @return {Boolean}
|
||||
*/
|
||||
this.is_undetermined = function (obj) {
|
||||
obj = this.get_node(obj);
|
||||
var s = this.settings.checkbox.cascade, i, j, t = this.settings.checkbox.tie_selection, d = this._data[ t ? 'core' : 'checkbox' ].selected, m = this._model.data;
|
||||
if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) {
|
||||
return false;
|
||||
}
|
||||
if(!obj.state.loaded && obj.original.state.undetermined === true) {
|
||||
return true;
|
||||
}
|
||||
for(i = 0, j = obj.children_d.length; i < j; i++) {
|
||||
if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* disable a node's checkbox
|
||||
* @name disable_checkbox(obj)
|
||||
* @param {mixed} obj an array can be used too
|
||||
* @trigger disable_checkbox.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.disable_checkbox = function (obj) {
|
||||
var t1, t2, dom;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.disable_checkbox(obj[t1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) {
|
||||
return false;
|
||||
}
|
||||
dom = this.get_node(obj, true);
|
||||
if(!obj.state.checkbox_disabled) {
|
||||
obj.state.checkbox_disabled = true;
|
||||
if(dom && dom.length) {
|
||||
dom.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-checkbox-disabled');
|
||||
}
|
||||
/**
|
||||
* triggered when an node's checkbox is disabled
|
||||
* @event
|
||||
* @name disable_checkbox.jstree
|
||||
* @param {Object} node
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('disable_checkbox', { 'node' : obj });
|
||||
}
|
||||
};
|
||||
/**
|
||||
* enable a node's checkbox
|
||||
* @name disable_checkbox(obj)
|
||||
* @param {mixed} obj an array can be used too
|
||||
* @trigger enable_checkbox.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.enable_checkbox = function (obj) {
|
||||
var t1, t2, dom;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.enable_checkbox(obj[t1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) {
|
||||
return false;
|
||||
}
|
||||
dom = this.get_node(obj, true);
|
||||
if(obj.state.checkbox_disabled) {
|
||||
obj.state.checkbox_disabled = false;
|
||||
if(dom && dom.length) {
|
||||
dom.children('.jstree-anchor').children('.jstree-checkbox').removeClass('jstree-checkbox-disabled');
|
||||
}
|
||||
/**
|
||||
* triggered when an node's checkbox is enabled
|
||||
* @event
|
||||
* @name enable_checkbox.jstree
|
||||
* @param {Object} node
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('enable_checkbox', { 'node' : obj });
|
||||
}
|
||||
};
|
||||
|
||||
this.activate_node = function (obj, e) {
|
||||
if($(e.target).hasClass('jstree-checkbox-disabled')) {
|
||||
return false;
|
||||
}
|
||||
if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) {
|
||||
e.ctrlKey = true;
|
||||
}
|
||||
if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) {
|
||||
return parent.activate_node.call(this, obj, e);
|
||||
}
|
||||
if(this.is_disabled(obj)) {
|
||||
return false;
|
||||
}
|
||||
if(this.is_checked(obj)) {
|
||||
this.uncheck_node(obj, e);
|
||||
}
|
||||
else {
|
||||
this.check_node(obj, e);
|
||||
}
|
||||
this.trigger('activate_node', { 'node' : this.get_node(obj) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Cascades checked state to a node and all its descendants. This function does NOT affect hidden and disabled nodes (or their descendants).
|
||||
* However if these unaffected nodes are already selected their ids will be included in the returned array.
|
||||
* @private
|
||||
* @param {string} id the node ID
|
||||
* @param {bool} checkedState should the nodes be checked or not
|
||||
* @returns {Array} Array of all node id's (in this tree branch) that are checked.
|
||||
*/
|
||||
this._cascade_new_checked_state = function (id, checkedState) {
|
||||
var self = this;
|
||||
var t = this.settings.checkbox.tie_selection;
|
||||
var node = this._model.data[id];
|
||||
var selectedNodeIds = [];
|
||||
var selectedChildrenIds = [], i, j, selectedChildIds;
|
||||
|
||||
if (
|
||||
(this.settings.checkbox.cascade_to_disabled || !node.state.disabled) &&
|
||||
(this.settings.checkbox.cascade_to_hidden || !node.state.hidden)
|
||||
) {
|
||||
//First try and check/uncheck the children
|
||||
if (node.children) {
|
||||
for (i = 0, j = node.children.length; i < j; i++) {
|
||||
var childId = node.children[i];
|
||||
selectedChildIds = self._cascade_new_checked_state(childId, checkedState);
|
||||
selectedNodeIds = selectedNodeIds.concat(selectedChildIds);
|
||||
if (selectedChildIds.indexOf(childId) > -1) {
|
||||
selectedChildrenIds.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dom = self.get_node(node, true);
|
||||
|
||||
//A node's state is undetermined if some but not all of it's children are checked/selected .
|
||||
var undetermined = selectedChildrenIds.length > 0 && selectedChildrenIds.length < node.children.length;
|
||||
|
||||
if(node.original && node.original.state && node.original.state.undetermined) {
|
||||
node.original.state.undetermined = undetermined;
|
||||
}
|
||||
|
||||
//If a node is undetermined then remove selected class
|
||||
if (undetermined) {
|
||||
node.state[ t ? 'selected' : 'checked' ] = false;
|
||||
dom.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
//Otherwise, if the checkedState === true (i.e. the node is being checked now) and all of the node's children are checked (if it has any children),
|
||||
//check the node and style it correctly.
|
||||
else if (checkedState && selectedChildrenIds.length === node.children.length) {
|
||||
node.state[ t ? 'selected' : 'checked' ] = checkedState;
|
||||
selectedNodeIds.push(node.id);
|
||||
|
||||
dom.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
else {
|
||||
node.state[ t ? 'selected' : 'checked' ] = false;
|
||||
dom.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
|
||||
}
|
||||
}
|
||||
else {
|
||||
selectedChildIds = this.get_checked_descendants(id);
|
||||
|
||||
if (node.state[ t ? 'selected' : 'checked' ]) {
|
||||
selectedChildIds.push(node.id);
|
||||
}
|
||||
|
||||
selectedNodeIds = selectedNodeIds.concat(selectedChildIds);
|
||||
}
|
||||
|
||||
return selectedNodeIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets ids of nodes selected in branch (of tree) specified by id (does not include the node specified by id)
|
||||
* @name get_checked_descendants(obj)
|
||||
* @param {string} id the node ID
|
||||
* @return {Array} array of IDs
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.get_checked_descendants = function (id) {
|
||||
var self = this;
|
||||
var t = self.settings.checkbox.tie_selection;
|
||||
var node = self._model.data[id];
|
||||
|
||||
return node.children_d.filter(function(_id) {
|
||||
return self._model.data[_id].state[ t ? 'selected' : 'checked' ];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally)
|
||||
* @name check_node(obj)
|
||||
* @param {mixed} obj an array can be used to check multiple nodes
|
||||
* @trigger check_node.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.check_node = function (obj, e) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); }
|
||||
var dom, t1, t2, th;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.check_node(obj[t1], e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) {
|
||||
return false;
|
||||
}
|
||||
dom = this.get_node(obj, true);
|
||||
if(!obj.state.checked) {
|
||||
obj.state.checked = true;
|
||||
this._data.checkbox.selected.push(obj.id);
|
||||
if(dom && dom.length) {
|
||||
dom.children('.jstree-anchor').addClass('jstree-checked');
|
||||
}
|
||||
/**
|
||||
* triggered when an node is checked (only if tie_selection in checkbox settings is false)
|
||||
* @event
|
||||
* @name check_node.jstree
|
||||
* @param {Object} node
|
||||
* @param {Array} selected the current selection
|
||||
* @param {Object} event the event (if any) that triggered this check_node
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
|
||||
}
|
||||
};
|
||||
/**
|
||||
* uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally)
|
||||
* @name uncheck_node(obj)
|
||||
* @param {mixed} obj an array can be used to uncheck multiple nodes
|
||||
* @trigger uncheck_node.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.uncheck_node = function (obj, e) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); }
|
||||
var t1, t2, dom;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.uncheck_node(obj[t1], e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) {
|
||||
return false;
|
||||
}
|
||||
dom = this.get_node(obj, true);
|
||||
if(obj.state.checked) {
|
||||
obj.state.checked = false;
|
||||
this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id);
|
||||
if(dom.length) {
|
||||
dom.children('.jstree-anchor').removeClass('jstree-checked');
|
||||
}
|
||||
/**
|
||||
* triggered when an node is unchecked (only if tie_selection in checkbox settings is false)
|
||||
* @event
|
||||
* @name uncheck_node.jstree
|
||||
* @param {Object} node
|
||||
* @param {Array} selected the current selection
|
||||
* @param {Object} event the event (if any) that triggered this uncheck_node
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally)
|
||||
* @name check_all()
|
||||
* @trigger check_all.jstree, changed.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.check_all = function () {
|
||||
if(this.settings.checkbox.tie_selection) { return this.select_all(); }
|
||||
var tmp = this._data.checkbox.selected.concat([]), i, j;
|
||||
this._data.checkbox.selected = this._model.data[$.jstree.root].children_d.concat();
|
||||
for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
|
||||
if(this._model.data[this._data.checkbox.selected[i]]) {
|
||||
this._model.data[this._data.checkbox.selected[i]].state.checked = true;
|
||||
}
|
||||
}
|
||||
this.redraw(true);
|
||||
/**
|
||||
* triggered when all nodes are checked (only if tie_selection in checkbox settings is false)
|
||||
* @event
|
||||
* @name check_all.jstree
|
||||
* @param {Array} selected the current selection
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('check_all', { 'selected' : this._data.checkbox.selected });
|
||||
};
|
||||
/**
|
||||
* uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally)
|
||||
* @name uncheck_all()
|
||||
* @trigger uncheck_all.jstree
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.uncheck_all = function () {
|
||||
if(this.settings.checkbox.tie_selection) { return this.deselect_all(); }
|
||||
var tmp = this._data.checkbox.selected.concat([]), i, j;
|
||||
for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
|
||||
if(this._model.data[this._data.checkbox.selected[i]]) {
|
||||
this._model.data[this._data.checkbox.selected[i]].state.checked = false;
|
||||
}
|
||||
}
|
||||
this._data.checkbox.selected = [];
|
||||
this.element.find('.jstree-checked').removeClass('jstree-checked');
|
||||
/**
|
||||
* triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false)
|
||||
* @event
|
||||
* @name uncheck_all.jstree
|
||||
* @param {Object} node the previous selection
|
||||
* @param {Array} selected the current selection
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp });
|
||||
};
|
||||
/**
|
||||
* checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected)
|
||||
* @name is_checked(obj)
|
||||
* @param {mixed} obj
|
||||
* @return {Boolean}
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.is_checked = function (obj) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); }
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) { return false; }
|
||||
return obj.state.checked;
|
||||
};
|
||||
/**
|
||||
* get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected)
|
||||
* @name get_checked([full])
|
||||
* @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
|
||||
* @return {Array}
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.get_checked = function (full) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.get_selected(full); }
|
||||
return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected;
|
||||
};
|
||||
/**
|
||||
* get an array of all top level checked nodes (ignoring children of checked nodes) (if tie_selection is on in the settings this function will return the same as get_top_selected)
|
||||
* @name get_top_checked([full])
|
||||
* @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
|
||||
* @return {Array}
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.get_top_checked = function (full) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); }
|
||||
var tmp = this.get_checked(true),
|
||||
obj = {}, i, j, k, l;
|
||||
for(i = 0, j = tmp.length; i < j; i++) {
|
||||
obj[tmp[i].id] = tmp[i];
|
||||
}
|
||||
for(i = 0, j = tmp.length; i < j; i++) {
|
||||
for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
|
||||
if(obj[tmp[i].children_d[k]]) {
|
||||
delete obj[tmp[i].children_d[k]];
|
||||
}
|
||||
}
|
||||
}
|
||||
tmp = [];
|
||||
for(i in obj) {
|
||||
if(obj.hasOwnProperty(i)) {
|
||||
tmp.push(i);
|
||||
}
|
||||
}
|
||||
return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
|
||||
};
|
||||
/**
|
||||
* get an array of all bottom level checked nodes (ignoring selected parents) (if tie_selection is on in the settings this function will return the same as get_bottom_selected)
|
||||
* @name get_bottom_checked([full])
|
||||
* @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
|
||||
* @return {Array}
|
||||
* @plugin checkbox
|
||||
*/
|
||||
this.get_bottom_checked = function (full) {
|
||||
if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); }
|
||||
var tmp = this.get_checked(true),
|
||||
obj = [], i, j;
|
||||
for(i = 0, j = tmp.length; i < j; i++) {
|
||||
if(!tmp[i].children.length) {
|
||||
obj.push(tmp[i].id);
|
||||
}
|
||||
}
|
||||
return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
|
||||
};
|
||||
this.load_node = function (obj, callback) {
|
||||
var k, l, i, j, c, tmp;
|
||||
if(!$.isArray(obj) && !this.settings.checkbox.tie_selection) {
|
||||
tmp = this.get_node(obj);
|
||||
if(tmp && tmp.state.loaded) {
|
||||
for(k = 0, l = tmp.children_d.length; k < l; k++) {
|
||||
if(this._model.data[tmp.children_d[k]].state.checked) {
|
||||
c = true;
|
||||
this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, tmp.children_d[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parent.load_node.apply(this, arguments);
|
||||
};
|
||||
this.get_state = function () {
|
||||
var state = parent.get_state.apply(this, arguments);
|
||||
if(this.settings.checkbox.tie_selection) { return state; }
|
||||
state.checkbox = this._data.checkbox.selected.slice();
|
||||
return state;
|
||||
};
|
||||
this.set_state = function (state, callback) {
|
||||
var res = parent.set_state.apply(this, arguments);
|
||||
if(res && state.checkbox) {
|
||||
if(!this.settings.checkbox.tie_selection) {
|
||||
this.uncheck_all();
|
||||
var _this = this;
|
||||
$.each(state.checkbox, function (i, v) {
|
||||
_this.check_node(v);
|
||||
});
|
||||
}
|
||||
delete state.checkbox;
|
||||
this.set_state(state, callback);
|
||||
return false;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
this.refresh = function (skip_loading, forget_state) {
|
||||
if(this.settings.checkbox.tie_selection) {
|
||||
this._data.checkbox.selected = [];
|
||||
}
|
||||
return parent.refresh.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// include the checkbox plugin by default
|
||||
// $.jstree.defaults.plugins.push("checkbox");
|
||||
}));
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* ### Conditionalselect plugin
|
||||
*
|
||||
* This plugin allows defining a callback to allow or deny node selection by user input (activate node method).
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.conditionalselect', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.conditionalselect) { return; }
|
||||
|
||||
/**
|
||||
* a callback (function) which is invoked in the instance's scope and receives two arguments - the node and the event that triggered the `activate_node` call. Returning false prevents working with the node, returning true allows invoking activate_node. Defaults to returning `true`.
|
||||
* @name $.jstree.defaults.checkbox.visible
|
||||
* @plugin checkbox
|
||||
*/
|
||||
$.jstree.defaults.conditionalselect = function () { return true; };
|
||||
$.jstree.plugins.conditionalselect = function (options, parent) {
|
||||
// own function
|
||||
this.activate_node = function (obj, e) {
|
||||
if(this.settings.conditionalselect.call(this, this.get_node(obj), e)) {
|
||||
return parent.activate_node.call(this, obj, e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}));
|
@ -0,0 +1,661 @@
|
||||
/**
|
||||
* ### Contextmenu plugin
|
||||
*
|
||||
* Shows a context menu when a node is right-clicked.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.contextmenu', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.contextmenu) { return; }
|
||||
|
||||
/**
|
||||
* stores all defaults for the contextmenu plugin
|
||||
* @name $.jstree.defaults.contextmenu
|
||||
* @plugin contextmenu
|
||||
*/
|
||||
$.jstree.defaults.contextmenu = {
|
||||
/**
|
||||
* a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`.
|
||||
* @name $.jstree.defaults.contextmenu.select_node
|
||||
* @plugin contextmenu
|
||||
*/
|
||||
select_node : true,
|
||||
/**
|
||||
* a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used.
|
||||
* @name $.jstree.defaults.contextmenu.show_at_node
|
||||
* @plugin contextmenu
|
||||
*/
|
||||
show_at_node : true,
|
||||
/**
|
||||
* an object of actions, or a function that accepts a node and a callback function and calls the callback function with an object of actions available for that node (you can also return the items too).
|
||||
*
|
||||
* Each action consists of a key (a unique name) and a value which is an object with the following properties (only label and action are required). Once a menu item is activated the `action` function will be invoked with an object containing the following keys: item - the contextmenu item definition as seen below, reference - the DOM node that was used (the tree node), element - the contextmenu DOM element, position - an object with x/y properties indicating the position of the menu.
|
||||
*
|
||||
* * `separator_before` - a boolean indicating if there should be a separator before this item
|
||||
* * `separator_after` - a boolean indicating if there should be a separator after this item
|
||||
* * `_disabled` - a boolean indicating if this action should be disabled
|
||||
* * `label` - a string - the name of the action (could be a function returning a string)
|
||||
* * `title` - a string - an optional tooltip for the item
|
||||
* * `action` - a function to be executed if this item is chosen, the function will receive
|
||||
* * `icon` - a string, can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class
|
||||
* * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2)
|
||||
* * `shortcut_label` - shortcut label (like for example `F2` for rename)
|
||||
* * `submenu` - an object with the same structure as $.jstree.defaults.contextmenu.items which can be used to create a submenu - each key will be rendered as a separate option in a submenu that will appear once the current item is hovered
|
||||
*
|
||||
* @name $.jstree.defaults.contextmenu.items
|
||||
* @plugin contextmenu
|
||||
*/
|
||||
items : function (o, cb) { // Could be an object directly
|
||||
return {
|
||||
"create" : {
|
||||
"separator_before" : false,
|
||||
"separator_after" : true,
|
||||
"_disabled" : false, //(this.check("create_node", data.reference, {}, "last")),
|
||||
"label" : "Create",
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
inst.create_node(obj, {}, "last", function (new_node) {
|
||||
try {
|
||||
inst.edit(new_node);
|
||||
} catch (ex) {
|
||||
setTimeout(function () { inst.edit(new_node); },0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"rename" : {
|
||||
"separator_before" : false,
|
||||
"separator_after" : false,
|
||||
"_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
|
||||
"label" : "Rename",
|
||||
/*!
|
||||
"shortcut" : 113,
|
||||
"shortcut_label" : 'F2',
|
||||
"icon" : "glyphicon glyphicon-leaf",
|
||||
*/
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
inst.edit(obj);
|
||||
}
|
||||
},
|
||||
"remove" : {
|
||||
"separator_before" : false,
|
||||
"icon" : false,
|
||||
"separator_after" : false,
|
||||
"_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
|
||||
"label" : "Delete",
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
if(inst.is_selected(obj)) {
|
||||
inst.delete_node(inst.get_selected());
|
||||
}
|
||||
else {
|
||||
inst.delete_node(obj);
|
||||
}
|
||||
}
|
||||
},
|
||||
"ccp" : {
|
||||
"separator_before" : true,
|
||||
"icon" : false,
|
||||
"separator_after" : false,
|
||||
"label" : "Edit",
|
||||
"action" : false,
|
||||
"submenu" : {
|
||||
"cut" : {
|
||||
"separator_before" : false,
|
||||
"separator_after" : false,
|
||||
"label" : "Cut",
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
if(inst.is_selected(obj)) {
|
||||
inst.cut(inst.get_top_selected());
|
||||
}
|
||||
else {
|
||||
inst.cut(obj);
|
||||
}
|
||||
}
|
||||
},
|
||||
"copy" : {
|
||||
"separator_before" : false,
|
||||
"icon" : false,
|
||||
"separator_after" : false,
|
||||
"label" : "Copy",
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
if(inst.is_selected(obj)) {
|
||||
inst.copy(inst.get_top_selected());
|
||||
}
|
||||
else {
|
||||
inst.copy(obj);
|
||||
}
|
||||
}
|
||||
},
|
||||
"paste" : {
|
||||
"separator_before" : false,
|
||||
"icon" : false,
|
||||
"_disabled" : function (data) {
|
||||
return !$.jstree.reference(data.reference).can_paste();
|
||||
},
|
||||
"separator_after" : false,
|
||||
"label" : "Paste",
|
||||
"action" : function (data) {
|
||||
var inst = $.jstree.reference(data.reference),
|
||||
obj = inst.get_node(data.reference);
|
||||
inst.paste(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$.jstree.plugins.contextmenu = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
|
||||
var last_ts = 0, cto = null, ex, ey;
|
||||
this.element
|
||||
.on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
|
||||
this.get_container_ul().addClass('jstree-contextmenu');
|
||||
}, this))
|
||||
.on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e, data) {
|
||||
if (e.target.tagName.toLowerCase() === 'input') {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
last_ts = e.ctrlKey ? +new Date() : 0;
|
||||
if(data || cto) {
|
||||
last_ts = (+new Date()) + 10000;
|
||||
}
|
||||
if(cto) {
|
||||
clearTimeout(cto);
|
||||
}
|
||||
if(!this.is_loading(e.currentTarget)) {
|
||||
this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e);
|
||||
}
|
||||
}, this))
|
||||
.on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
|
||||
if(this._data.contextmenu.visible && (!last_ts || (+new Date()) - last_ts > 250)) { // work around safari & macOS ctrl+click
|
||||
$.vakata.context.hide();
|
||||
}
|
||||
last_ts = 0;
|
||||
}, this))
|
||||
.on("touchstart.jstree", ".jstree-anchor", function (e) {
|
||||
if(!e.originalEvent || !e.originalEvent.changedTouches || !e.originalEvent.changedTouches[0]) {
|
||||
return;
|
||||
}
|
||||
ex = e.originalEvent.changedTouches[0].clientX;
|
||||
ey = e.originalEvent.changedTouches[0].clientY;
|
||||
cto = setTimeout(function () {
|
||||
$(e.currentTarget).trigger('contextmenu', true);
|
||||
}, 750);
|
||||
})
|
||||
.on('touchmove.vakata.jstree', function (e) {
|
||||
if(cto && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0] && (Math.abs(ex - e.originalEvent.changedTouches[0].clientX) > 10 || Math.abs(ey - e.originalEvent.changedTouches[0].clientY) > 10)) {
|
||||
clearTimeout(cto);
|
||||
$.vakata.context.hide();
|
||||
}
|
||||
})
|
||||
.on('touchend.vakata.jstree', function (e) {
|
||||
if(cto) {
|
||||
clearTimeout(cto);
|
||||
}
|
||||
});
|
||||
|
||||
/*!
|
||||
if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) {
|
||||
var el = null, tm = null;
|
||||
this.element
|
||||
.on("touchstart", ".jstree-anchor", function (e) {
|
||||
el = e.currentTarget;
|
||||
tm = +new Date();
|
||||
$(document).one("touchend", function (e) {
|
||||
e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset);
|
||||
e.currentTarget = e.target;
|
||||
tm = ((+(new Date())) - tm);
|
||||
if(e.target === el && tm > 600 && tm < 1000) {
|
||||
e.preventDefault();
|
||||
$(el).trigger('contextmenu', e);
|
||||
}
|
||||
el = null;
|
||||
tm = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
$(document).on("context_hide.vakata.jstree", $.proxy(function (e, data) {
|
||||
this._data.contextmenu.visible = false;
|
||||
$(data.reference).removeClass('jstree-context');
|
||||
}, this));
|
||||
};
|
||||
this.teardown = function () {
|
||||
if(this._data.contextmenu.visible) {
|
||||
$.vakata.context.hide();
|
||||
}
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* prepare and show the context menu for a node
|
||||
* @name show_contextmenu(obj [, x, y])
|
||||
* @param {mixed} obj the node
|
||||
* @param {Number} x the x-coordinate relative to the document to show the menu at
|
||||
* @param {Number} y the y-coordinate relative to the document to show the menu at
|
||||
* @param {Object} e the event if available that triggered the contextmenu
|
||||
* @plugin contextmenu
|
||||
* @trigger show_contextmenu.jstree
|
||||
*/
|
||||
this.show_contextmenu = function (obj, x, y, e) {
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) { return false; }
|
||||
var s = this.settings.contextmenu,
|
||||
d = this.get_node(obj, true),
|
||||
a = d.children(".jstree-anchor"),
|
||||
o = false,
|
||||
i = false;
|
||||
if(s.show_at_node || x === undefined || y === undefined) {
|
||||
o = a.offset();
|
||||
x = o.left;
|
||||
y = o.top + this._data.core.li_height;
|
||||
}
|
||||
if(this.settings.contextmenu.select_node && !this.is_selected(obj)) {
|
||||
this.activate_node(obj, e);
|
||||
}
|
||||
|
||||
i = s.items;
|
||||
if($.isFunction(i)) {
|
||||
i = i.call(this, obj, $.proxy(function (i) {
|
||||
this._show_contextmenu(obj, x, y, i);
|
||||
}, this));
|
||||
}
|
||||
if($.isPlainObject(i)) {
|
||||
this._show_contextmenu(obj, x, y, i);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* show the prepared context menu for a node
|
||||
* @name _show_contextmenu(obj, x, y, i)
|
||||
* @param {mixed} obj the node
|
||||
* @param {Number} x the x-coordinate relative to the document to show the menu at
|
||||
* @param {Number} y the y-coordinate relative to the document to show the menu at
|
||||
* @param {Number} i the object of items to show
|
||||
* @plugin contextmenu
|
||||
* @trigger show_contextmenu.jstree
|
||||
* @private
|
||||
*/
|
||||
this._show_contextmenu = function (obj, x, y, i) {
|
||||
var d = this.get_node(obj, true),
|
||||
a = d.children(".jstree-anchor");
|
||||
$(document).one("context_show.vakata.jstree", $.proxy(function (e, data) {
|
||||
var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu';
|
||||
$(data.element).addClass(cls);
|
||||
a.addClass('jstree-context');
|
||||
}, this));
|
||||
this._data.contextmenu.visible = true;
|
||||
$.vakata.context.show(a, { 'x' : x, 'y' : y }, i);
|
||||
/**
|
||||
* triggered when the contextmenu is shown for a node
|
||||
* @event
|
||||
* @name show_contextmenu.jstree
|
||||
* @param {Object} node the node
|
||||
* @param {Number} x the x-coordinate of the menu relative to the document
|
||||
* @param {Number} y the y-coordinate of the menu relative to the document
|
||||
* @plugin contextmenu
|
||||
*/
|
||||
this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y });
|
||||
};
|
||||
};
|
||||
|
||||
// contextmenu helper
|
||||
(function ($) {
|
||||
var right_to_left = false,
|
||||
vakata_context = {
|
||||
element : false,
|
||||
reference : false,
|
||||
position_x : 0,
|
||||
position_y : 0,
|
||||
items : [],
|
||||
html : "",
|
||||
is_visible : false
|
||||
};
|
||||
|
||||
$.vakata.context = {
|
||||
settings : {
|
||||
hide_onmouseleave : 0,
|
||||
icons : true
|
||||
},
|
||||
_trigger : function (event_name) {
|
||||
$(document).triggerHandler("context_" + event_name + ".vakata", {
|
||||
"reference" : vakata_context.reference,
|
||||
"element" : vakata_context.element,
|
||||
"position" : {
|
||||
"x" : vakata_context.position_x,
|
||||
"y" : vakata_context.position_y
|
||||
}
|
||||
});
|
||||
},
|
||||
_execute : function (i) {
|
||||
i = vakata_context.items[i];
|
||||
return i && (!i._disabled || ($.isFunction(i._disabled) && !i._disabled({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }))) && i.action ? i.action.call(null, {
|
||||
"item" : i,
|
||||
"reference" : vakata_context.reference,
|
||||
"element" : vakata_context.element,
|
||||
"position" : {
|
||||
"x" : vakata_context.position_x,
|
||||
"y" : vakata_context.position_y
|
||||
}
|
||||
}) : false;
|
||||
},
|
||||
_parse : function (o, is_callback) {
|
||||
if(!o) { return false; }
|
||||
if(!is_callback) {
|
||||
vakata_context.html = "";
|
||||
vakata_context.items = [];
|
||||
}
|
||||
var str = "",
|
||||
sep = false,
|
||||
tmp;
|
||||
|
||||
if(is_callback) { str += "<"+"ul>"; }
|
||||
$.each(o, function (i, val) {
|
||||
if(!val) { return true; }
|
||||
vakata_context.items.push(val);
|
||||
if(!sep && val.separator_before) {
|
||||
str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
|
||||
}
|
||||
sep = false;
|
||||
str += "<"+"li class='" + (val._class || "") + (val._disabled === true || ($.isFunction(val._disabled) && val._disabled({ "item" : val, "reference" : vakata_context.reference, "element" : vakata_context.element })) ? " vakata-contextmenu-disabled " : "") + "' "+(val.shortcut?" data-shortcut='"+val.shortcut+"' ":'')+">";
|
||||
str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "' " + (val.title ? "title='" + val.title + "'" : "") + ">";
|
||||
if($.vakata.context.settings.icons) {
|
||||
str += "<"+"i ";
|
||||
if(val.icon) {
|
||||
if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; }
|
||||
else { str += " class='" + val.icon + "' "; }
|
||||
}
|
||||
str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>";
|
||||
}
|
||||
str += ($.isFunction(val.label) ? val.label({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }) : val.label) + (val.shortcut?' <span class="vakata-contextmenu-shortcut vakata-contextmenu-shortcut-'+val.shortcut+'">'+ (val.shortcut_label || '') +'</span>':'') + "<"+"/a>";
|
||||
if(val.submenu) {
|
||||
tmp = $.vakata.context._parse(val.submenu, true);
|
||||
if(tmp) { str += tmp; }
|
||||
}
|
||||
str += "<"+"/li>";
|
||||
if(val.separator_after) {
|
||||
str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
|
||||
sep = true;
|
||||
}
|
||||
});
|
||||
str = str.replace(/<li class\='vakata-context-separator'\><\/li\>$/,"");
|
||||
if(is_callback) { str += "</ul>"; }
|
||||
/**
|
||||
* triggered on the document when the contextmenu is parsed (HTML is built)
|
||||
* @event
|
||||
* @plugin contextmenu
|
||||
* @name context_parse.vakata
|
||||
* @param {jQuery} reference the element that was right clicked
|
||||
* @param {jQuery} element the DOM element of the menu itself
|
||||
* @param {Object} position the x & y coordinates of the menu
|
||||
*/
|
||||
if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); }
|
||||
return str.length > 10 ? str : false;
|
||||
},
|
||||
_show_submenu : function (o) {
|
||||
o = $(o);
|
||||
if(!o.length || !o.children("ul").length) { return; }
|
||||
var e = o.children("ul"),
|
||||
xl = o.offset().left,
|
||||
x = xl + o.outerWidth(),
|
||||
y = o.offset().top,
|
||||
w = e.width(),
|
||||
h = e.height(),
|
||||
dw = $(window).width() + $(window).scrollLeft(),
|
||||
dh = $(window).height() + $(window).scrollTop();
|
||||
// може да се спести е една проверка - дали няма някой от класовете вече нагоре
|
||||
if(right_to_left) {
|
||||
o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left");
|
||||
}
|
||||
else {
|
||||
o[x + w > dw && xl > dw - x ? "addClass" : "removeClass"]("vakata-context-right");
|
||||
}
|
||||
if(y + h + 10 > dh) {
|
||||
e.css("bottom","-1px");
|
||||
}
|
||||
|
||||
//if does not fit - stick it to the side
|
||||
if (o.hasClass('vakata-context-right')) {
|
||||
if (xl < w) {
|
||||
e.css("margin-right", xl - w);
|
||||
}
|
||||
} else {
|
||||
if (dw - x < w) {
|
||||
e.css("margin-left", dw - x - w);
|
||||
}
|
||||
}
|
||||
|
||||
e.show();
|
||||
},
|
||||
show : function (reference, position, data) {
|
||||
var o, e, x, y, w, h, dw, dh, cond = true;
|
||||
if(vakata_context.element && vakata_context.element.length) {
|
||||
vakata_context.element.width('');
|
||||
}
|
||||
switch(cond) {
|
||||
case (!position && !reference):
|
||||
return false;
|
||||
case (!!position && !!reference):
|
||||
vakata_context.reference = reference;
|
||||
vakata_context.position_x = position.x;
|
||||
vakata_context.position_y = position.y;
|
||||
break;
|
||||
case (!position && !!reference):
|
||||
vakata_context.reference = reference;
|
||||
o = reference.offset();
|
||||
vakata_context.position_x = o.left + reference.outerHeight();
|
||||
vakata_context.position_y = o.top;
|
||||
break;
|
||||
case (!!position && !reference):
|
||||
vakata_context.position_x = position.x;
|
||||
vakata_context.position_y = position.y;
|
||||
break;
|
||||
}
|
||||
if(!!reference && !data && $(reference).data('vakata_contextmenu')) {
|
||||
data = $(reference).data('vakata_contextmenu');
|
||||
}
|
||||
if($.vakata.context._parse(data)) {
|
||||
vakata_context.element.html(vakata_context.html);
|
||||
}
|
||||
if(vakata_context.items.length) {
|
||||
vakata_context.element.appendTo(document.body);
|
||||
e = vakata_context.element;
|
||||
x = vakata_context.position_x;
|
||||
y = vakata_context.position_y;
|
||||
w = e.width();
|
||||
h = e.height();
|
||||
dw = $(window).width() + $(window).scrollLeft();
|
||||
dh = $(window).height() + $(window).scrollTop();
|
||||
if(right_to_left) {
|
||||
x -= (e.outerWidth() - $(reference).outerWidth());
|
||||
if(x < $(window).scrollLeft() + 20) {
|
||||
x = $(window).scrollLeft() + 20;
|
||||
}
|
||||
}
|
||||
if(x + w + 20 > dw) {
|
||||
x = dw - (w + 20);
|
||||
}
|
||||
if(y + h + 20 > dh) {
|
||||
y = dh - (h + 20);
|
||||
}
|
||||
|
||||
vakata_context.element
|
||||
.css({ "left" : x, "top" : y })
|
||||
.show()
|
||||
.find('a').first().focus().parent().addClass("vakata-context-hover");
|
||||
vakata_context.is_visible = true;
|
||||
/**
|
||||
* triggered on the document when the contextmenu is shown
|
||||
* @event
|
||||
* @plugin contextmenu
|
||||
* @name context_show.vakata
|
||||
* @param {jQuery} reference the element that was right clicked
|
||||
* @param {jQuery} element the DOM element of the menu itself
|
||||
* @param {Object} position the x & y coordinates of the menu
|
||||
*/
|
||||
$.vakata.context._trigger("show");
|
||||
}
|
||||
},
|
||||
hide : function () {
|
||||
if(vakata_context.is_visible) {
|
||||
vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach();
|
||||
vakata_context.is_visible = false;
|
||||
/**
|
||||
* triggered on the document when the contextmenu is hidden
|
||||
* @event
|
||||
* @plugin contextmenu
|
||||
* @name context_hide.vakata
|
||||
* @param {jQuery} reference the element that was right clicked
|
||||
* @param {jQuery} element the DOM element of the menu itself
|
||||
* @param {Object} position the x & y coordinates of the menu
|
||||
*/
|
||||
$.vakata.context._trigger("hide");
|
||||
}
|
||||
}
|
||||
};
|
||||
$(function () {
|
||||
right_to_left = $(document.body).css("direction") === "rtl";
|
||||
var to = false;
|
||||
|
||||
vakata_context.element = $("<ul class='vakata-context'></ul>");
|
||||
vakata_context.element
|
||||
.on("mouseenter", "li", function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if($.contains(this, e.relatedTarget)) {
|
||||
// премахнато заради delegate mouseleave по-долу
|
||||
// $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
|
||||
return;
|
||||
}
|
||||
|
||||
if(to) { clearTimeout(to); }
|
||||
vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end();
|
||||
|
||||
$(this)
|
||||
.siblings().find("ul").hide().end().end()
|
||||
.parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover");
|
||||
$.vakata.context._show_submenu(this);
|
||||
})
|
||||
// тестово - дали не натоварва?
|
||||
.on("mouseleave", "li", function (e) {
|
||||
if($.contains(this, e.relatedTarget)) { return; }
|
||||
$(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover");
|
||||
})
|
||||
.on("mouseleave", function (e) {
|
||||
$(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
|
||||
if($.vakata.context.settings.hide_onmouseleave) {
|
||||
to = setTimeout(
|
||||
(function (t) {
|
||||
return function () { $.vakata.context.hide(); };
|
||||
}(this)), $.vakata.context.settings.hide_onmouseleave);
|
||||
}
|
||||
})
|
||||
.on("click", "a", function (e) {
|
||||
e.preventDefault();
|
||||
//})
|
||||
//.on("mouseup", "a", function (e) {
|
||||
if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) {
|
||||
$.vakata.context.hide();
|
||||
}
|
||||
})
|
||||
.on('keydown', 'a', function (e) {
|
||||
var o = null;
|
||||
switch(e.which) {
|
||||
case 13:
|
||||
case 32:
|
||||
e.type = "click";
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).trigger(e);
|
||||
break;
|
||||
case 37:
|
||||
if(vakata_context.is_visible) {
|
||||
vakata_context.element.find(".vakata-context-hover").last().closest("li").first().find("ul").hide().find(".vakata-context-hover").removeClass("vakata-context-hover").end().end().children('a').focus();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
case 38:
|
||||
if(vakata_context.is_visible) {
|
||||
o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first();
|
||||
if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); }
|
||||
o.addClass("vakata-context-hover").children('a').focus();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
case 39:
|
||||
if(vakata_context.is_visible) {
|
||||
vakata_context.element.find(".vakata-context-hover").last().children("ul").show().children("li:not(.vakata-context-separator)").removeClass("vakata-context-hover").first().addClass("vakata-context-hover").children('a').focus();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
case 40:
|
||||
if(vakata_context.is_visible) {
|
||||
o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first();
|
||||
if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); }
|
||||
o.addClass("vakata-context-hover").children('a').focus();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
case 27:
|
||||
$.vakata.context.hide();
|
||||
e.preventDefault();
|
||||
break;
|
||||
default:
|
||||
//console.log(e.which);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.on('keydown', function (e) {
|
||||
e.preventDefault();
|
||||
var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent();
|
||||
if(a.parent().not('.vakata-context-disabled')) {
|
||||
a.click();
|
||||
}
|
||||
});
|
||||
|
||||
$(document)
|
||||
.on("mousedown.vakata.jstree", function (e) {
|
||||
if(vakata_context.is_visible && vakata_context.element[0] !== e.target && !$.contains(vakata_context.element[0], e.target)) {
|
||||
$.vakata.context.hide();
|
||||
}
|
||||
})
|
||||
.on("context_show.vakata.jstree", function (e, data) {
|
||||
vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent");
|
||||
if(right_to_left) {
|
||||
vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl");
|
||||
}
|
||||
// also apply a RTL class?
|
||||
vakata_context.element.find("ul").hide().end();
|
||||
});
|
||||
});
|
||||
}($));
|
||||
// $.jstree.defaults.plugins.push("contextmenu");
|
||||
}));
|
@ -0,0 +1,669 @@
|
||||
/**
|
||||
* ### Drag'n'drop plugin
|
||||
*
|
||||
* Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.dnd', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.dnd) { return; }
|
||||
|
||||
/**
|
||||
* stores all defaults for the drag'n'drop plugin
|
||||
* @name $.jstree.defaults.dnd
|
||||
* @plugin dnd
|
||||
*/
|
||||
$.jstree.defaults.dnd = {
|
||||
/**
|
||||
* a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
|
||||
* @name $.jstree.defaults.dnd.copy
|
||||
* @plugin dnd
|
||||
*/
|
||||
copy : true,
|
||||
/**
|
||||
* a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
|
||||
* @name $.jstree.defaults.dnd.open_timeout
|
||||
* @plugin dnd
|
||||
*/
|
||||
open_timeout : 500,
|
||||
/**
|
||||
* a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) and the event that started the drag - return `false` to prevent dragging
|
||||
* @name $.jstree.defaults.dnd.is_draggable
|
||||
* @plugin dnd
|
||||
*/
|
||||
is_draggable : true,
|
||||
/**
|
||||
* a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true`
|
||||
* @name $.jstree.defaults.dnd.check_while_dragging
|
||||
* @plugin dnd
|
||||
*/
|
||||
check_while_dragging : true,
|
||||
/**
|
||||
* a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
|
||||
* @name $.jstree.defaults.dnd.always_copy
|
||||
* @plugin dnd
|
||||
*/
|
||||
always_copy : false,
|
||||
/**
|
||||
* when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0`
|
||||
* @name $.jstree.defaults.dnd.inside_pos
|
||||
* @plugin dnd
|
||||
*/
|
||||
inside_pos : 0,
|
||||
/**
|
||||
* when starting the drag on a node that is selected this setting controls if all selected nodes are dragged or only the single node, default is `true`, which means all selected nodes are dragged when the drag is started on a selected node
|
||||
* @name $.jstree.defaults.dnd.drag_selection
|
||||
* @plugin dnd
|
||||
*/
|
||||
drag_selection : true,
|
||||
/**
|
||||
* controls whether dnd works on touch devices. If left as boolean true dnd will work the same as in desktop browsers, which in some cases may impair scrolling. If set to boolean false dnd will not work on touch devices. There is a special third option - string "selected" which means only selected nodes can be dragged on touch devices.
|
||||
* @name $.jstree.defaults.dnd.touch
|
||||
* @plugin dnd
|
||||
*/
|
||||
touch : true,
|
||||
/**
|
||||
* controls whether items can be dropped anywhere on the node, not just on the anchor, by default only the node anchor is a valid drop target. Works best with the wholerow plugin. If enabled on mobile depending on the interface it might be hard for the user to cancel the drop, since the whole tree container will be a valid drop target.
|
||||
* @name $.jstree.defaults.dnd.large_drop_target
|
||||
* @plugin dnd
|
||||
*/
|
||||
large_drop_target : false,
|
||||
/**
|
||||
* controls whether a drag can be initiated from any part of the node and not just the text/icon part, works best with the wholerow plugin. Keep in mind it can cause problems with tree scrolling on mobile depending on the interface - in that case set the touch option to "selected".
|
||||
* @name $.jstree.defaults.dnd.large_drag_target
|
||||
* @plugin dnd
|
||||
*/
|
||||
large_drag_target : false,
|
||||
/**
|
||||
* controls whether use HTML5 dnd api instead of classical. That will allow better integration of dnd events with other HTML5 controls.
|
||||
* @reference http://caniuse.com/#feat=dragndrop
|
||||
* @name $.jstree.defaults.dnd.use_html5
|
||||
* @plugin dnd
|
||||
*/
|
||||
use_html5: false
|
||||
};
|
||||
var drg, elm;
|
||||
// TODO: now check works by checking for each node individually, how about max_children, unique, etc?
|
||||
$.jstree.plugins.dnd = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
parent.init.call(this, el, options);
|
||||
this.settings.dnd.use_html5 = this.settings.dnd.use_html5 && ('draggable' in document.createElement('span'));
|
||||
};
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
|
||||
this.element
|
||||
.on(this.settings.dnd.use_html5 ? 'dragstart.jstree' : 'mousedown.jstree touchstart.jstree', this.settings.dnd.large_drag_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) {
|
||||
if(this.settings.dnd.large_drag_target && $(e.target).closest('.jstree-node')[0] !== e.currentTarget) {
|
||||
return true;
|
||||
}
|
||||
if(e.type === "touchstart" && (!this.settings.dnd.touch || (this.settings.dnd.touch === 'selected' && !$(e.currentTarget).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) {
|
||||
return true;
|
||||
}
|
||||
var obj = this.get_node(e.target),
|
||||
mlt = this.is_selected(obj) && this.settings.dnd.drag_selection ? this.get_top_selected().length : 1,
|
||||
txt = (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget));
|
||||
if(this.settings.core.force_text) {
|
||||
txt = $.vakata.html.escape(txt);
|
||||
}
|
||||
if(obj && obj.id && obj.id !== $.jstree.root && (e.which === 1 || e.type === "touchstart" || e.type === "dragstart") &&
|
||||
(this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_top_selected(true) : [obj]), e)))
|
||||
) {
|
||||
drg = { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_top_selected() : [obj.id] };
|
||||
elm = e.currentTarget;
|
||||
if (this.settings.dnd.use_html5) {
|
||||
$.vakata.dnd._trigger('start', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
} else {
|
||||
this.element.trigger('mousedown.jstree');
|
||||
return $.vakata.dnd.start(e, drg, '<div id="jstree-dnd" class="jstree-' + this.get_theme() + ' jstree-' + this.get_theme() + '-' + this.get_theme_variant() + ' ' + ( this.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ) + '"><i class="jstree-icon jstree-er"></i>' + txt + '<ins class="jstree-copy" style="display:none;">+</ins></div>');
|
||||
}
|
||||
}
|
||||
}, this));
|
||||
if (this.settings.dnd.use_html5) {
|
||||
this.element
|
||||
.on('dragover.jstree', function (e) {
|
||||
e.preventDefault();
|
||||
$.vakata.dnd._trigger('move', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
return false;
|
||||
})
|
||||
//.on('dragenter.jstree', this.settings.dnd.large_drop_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) {
|
||||
// e.preventDefault();
|
||||
// $.vakata.dnd._trigger('move', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
// return false;
|
||||
// }, this))
|
||||
.on('drop.jstree', $.proxy(function (e) {
|
||||
e.preventDefault();
|
||||
$.vakata.dnd._trigger('stop', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
return false;
|
||||
}, this));
|
||||
}
|
||||
};
|
||||
this.redraw_node = function(obj, deep, callback, force_render) {
|
||||
obj = parent.redraw_node.apply(this, arguments);
|
||||
if (obj && this.settings.dnd.use_html5) {
|
||||
if (this.settings.dnd.large_drag_target) {
|
||||
obj.setAttribute('draggable', true);
|
||||
} else {
|
||||
var i, j, tmp = null;
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
tmp.setAttribute('draggable', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
|
||||
$(function() {
|
||||
// bind only once for all instances
|
||||
var lastmv = false,
|
||||
laster = false,
|
||||
lastev = false,
|
||||
opento = false,
|
||||
marker = $('<div id="jstree-marker"> </div>').hide(); //.appendTo('body');
|
||||
|
||||
$(document)
|
||||
.on('dragover.vakata.jstree', function (e) {
|
||||
if (elm) {
|
||||
$.vakata.dnd._trigger('move', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
}
|
||||
})
|
||||
.on('drop.vakata.jstree', function (e) {
|
||||
if (elm) {
|
||||
$.vakata.dnd._trigger('stop', e, { 'helper': $(), 'element': elm, 'data': drg });
|
||||
elm = null;
|
||||
drg = null;
|
||||
}
|
||||
})
|
||||
.on('dnd_start.vakata.jstree', function (e, data) {
|
||||
lastmv = false;
|
||||
lastev = false;
|
||||
if(!data || !data.data || !data.data.jstree) { return; }
|
||||
marker.appendTo(document.body); //.show();
|
||||
})
|
||||
.on('dnd_move.vakata.jstree', function (e, data) {
|
||||
var isDifferentNode = data.event.target !== lastev.target;
|
||||
if(opento) {
|
||||
if (!data.event || data.event.type !== 'dragover' || isDifferentNode) {
|
||||
clearTimeout(opento);
|
||||
}
|
||||
}
|
||||
if(!data || !data.data || !data.data.jstree) { return; }
|
||||
|
||||
// if we are hovering the marker image do nothing (can happen on "inside" drags)
|
||||
if(data.event.target.id && data.event.target.id === 'jstree-marker') {
|
||||
return;
|
||||
}
|
||||
lastev = data.event;
|
||||
|
||||
var ins = $.jstree.reference(data.event.target),
|
||||
ref = false,
|
||||
off = false,
|
||||
rel = false,
|
||||
tmp, l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm, is_copy, pn;
|
||||
// if we are over an instance
|
||||
if(ins && ins._data && ins._data.dnd) {
|
||||
marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ));
|
||||
is_copy = data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)));
|
||||
data.helper
|
||||
.children().attr('class', 'jstree-' + ins.get_theme() + ' jstree-' + ins.get_theme() + '-' + ins.get_theme_variant() + ' ' + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ))
|
||||
.find('.jstree-copy').first()[ is_copy ? 'show' : 'hide' ]();
|
||||
|
||||
// if are hovering the container itself add a new root node
|
||||
//console.log(data.event);
|
||||
if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) {
|
||||
ok = true;
|
||||
for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
|
||||
ok = ok && ins.check( (data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)) ) ? "copy_node" : "move_node"), (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), $.jstree.root, 'last', { 'dnd' : true, 'ref' : ins.get_node($.jstree.root), 'pos' : 'i', 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) });
|
||||
if(!ok) { break; }
|
||||
}
|
||||
if(ok) {
|
||||
lastmv = { 'ins' : ins, 'par' : $.jstree.root, 'pos' : 'last' };
|
||||
marker.hide();
|
||||
data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
|
||||
if (data.event.originalEvent && data.event.originalEvent.dataTransfer) {
|
||||
data.event.originalEvent.dataTransfer.dropEffect = is_copy ? 'copy' : 'move';
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// if we are hovering a tree node
|
||||
ref = ins.settings.dnd.large_drop_target ? $(data.event.target).closest('.jstree-node').children('.jstree-anchor') : $(data.event.target).closest('.jstree-anchor');
|
||||
if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) {
|
||||
off = ref.offset();
|
||||
rel = (data.event.pageY !== undefined ? data.event.pageY : data.event.originalEvent.pageY) - off.top;
|
||||
h = ref.outerHeight();
|
||||
if(rel < h / 3) {
|
||||
o = ['b', 'i', 'a'];
|
||||
}
|
||||
else if(rel > h - h / 3) {
|
||||
o = ['a', 'i', 'b'];
|
||||
}
|
||||
else {
|
||||
o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a'];
|
||||
}
|
||||
$.each(o, function (j, v) {
|
||||
switch(v) {
|
||||
case 'b':
|
||||
l = off.left - 6;
|
||||
t = off.top;
|
||||
p = ins.get_parent(ref);
|
||||
i = ref.parent().index();
|
||||
break;
|
||||
case 'i':
|
||||
ip = ins.settings.dnd.inside_pos;
|
||||
tm = ins.get_node(ref.parent());
|
||||
l = off.left - 2;
|
||||
t = off.top + h / 2 + 1;
|
||||
p = tm.id;
|
||||
i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length));
|
||||
break;
|
||||
case 'a':
|
||||
l = off.left - 6;
|
||||
t = off.top + h;
|
||||
p = ins.get_parent(ref);
|
||||
i = ref.parent().index() + 1;
|
||||
break;
|
||||
}
|
||||
ok = true;
|
||||
for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
|
||||
op = data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? "copy_node" : "move_node";
|
||||
ps = i;
|
||||
if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) {
|
||||
pr = ins.get_node(p);
|
||||
if(ps > $.inArray(data.data.nodes[t1], pr.children)) {
|
||||
ps -= 1;
|
||||
}
|
||||
}
|
||||
ok = ok && ( (ins && ins.settings && ins.settings.dnd && ins.settings.dnd.check_while_dragging === false) || ins.check(op, (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), p, ps, { 'dnd' : true, 'ref' : ins.get_node(ref.parent()), 'pos' : v, 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }) );
|
||||
if(!ok) {
|
||||
if(ins && ins.last_error) { laster = ins.last_error(); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) {
|
||||
if (!data.event || data.event.type !== 'dragover' || isDifferentNode) {
|
||||
if (opento) { clearTimeout(opento); }
|
||||
opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout);
|
||||
}
|
||||
}
|
||||
if(ok) {
|
||||
pn = ins.get_node(p, true);
|
||||
if (!pn.hasClass('.jstree-dnd-parent')) {
|
||||
$('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
|
||||
pn.addClass('jstree-dnd-parent');
|
||||
}
|
||||
lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i };
|
||||
marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show();
|
||||
data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
|
||||
if (data.event.originalEvent && data.event.originalEvent.dataTransfer) {
|
||||
data.event.originalEvent.dataTransfer.dropEffect = is_copy ? 'copy' : 'move';
|
||||
}
|
||||
laster = {};
|
||||
o = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(o === true) { return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
$('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
|
||||
lastmv = false;
|
||||
data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er');
|
||||
if (data.event.originalEvent && data.event.originalEvent.dataTransfer) {
|
||||
//data.event.originalEvent.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
marker.hide();
|
||||
})
|
||||
.on('dnd_scroll.vakata.jstree', function (e, data) {
|
||||
if(!data || !data.data || !data.data.jstree) { return; }
|
||||
marker.hide();
|
||||
lastmv = false;
|
||||
lastev = false;
|
||||
data.helper.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er');
|
||||
})
|
||||
.on('dnd_stop.vakata.jstree', function (e, data) {
|
||||
$('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
|
||||
if(opento) { clearTimeout(opento); }
|
||||
if(!data || !data.data || !data.data.jstree) { return; }
|
||||
marker.hide().detach();
|
||||
var i, j, nodes = [];
|
||||
if(lastmv) {
|
||||
for(i = 0, j = data.data.nodes.length; i < j; i++) {
|
||||
nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i];
|
||||
}
|
||||
lastmv.ins[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'copy_node' : 'move_node' ](nodes, lastmv.par, lastmv.pos, false, false, false, data.data.origin);
|
||||
}
|
||||
else {
|
||||
i = $(data.event.target).closest('.jstree');
|
||||
if(i.length && laster && laster.error && laster.error === 'check') {
|
||||
i = i.jstree(true);
|
||||
if(i) {
|
||||
i.settings.core.error.call(this, laster);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastev = false;
|
||||
lastmv = false;
|
||||
})
|
||||
.on('keyup.jstree keydown.jstree', function (e, data) {
|
||||
data = $.vakata.dnd._get();
|
||||
if(data && data.data && data.data.jstree) {
|
||||
if (e.type === "keyup" && e.which === 27) {
|
||||
if (opento) { clearTimeout(opento); }
|
||||
lastmv = false;
|
||||
laster = false;
|
||||
lastev = false;
|
||||
opento = false;
|
||||
marker.hide().detach();
|
||||
$.vakata.dnd._clean();
|
||||
} else {
|
||||
data.helper.find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (e.metaKey || e.ctrlKey))) ? 'show' : 'hide' ]();
|
||||
if(lastev) {
|
||||
lastev.metaKey = e.metaKey;
|
||||
lastev.ctrlKey = e.ctrlKey;
|
||||
$.vakata.dnd._trigger('move', lastev);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// helpers
|
||||
(function ($) {
|
||||
$.vakata.html = {
|
||||
div : $('<div />'),
|
||||
escape : function (str) {
|
||||
return $.vakata.html.div.text(str).html();
|
||||
},
|
||||
strip : function (str) {
|
||||
return $.vakata.html.div.empty().append($.parseHTML(str)).text();
|
||||
}
|
||||
};
|
||||
// private variable
|
||||
var vakata_dnd = {
|
||||
element : false,
|
||||
target : false,
|
||||
is_down : false,
|
||||
is_drag : false,
|
||||
helper : false,
|
||||
helper_w: 0,
|
||||
data : false,
|
||||
init_x : 0,
|
||||
init_y : 0,
|
||||
scroll_l: 0,
|
||||
scroll_t: 0,
|
||||
scroll_e: false,
|
||||
scroll_i: false,
|
||||
is_touch: false
|
||||
};
|
||||
$.vakata.dnd = {
|
||||
settings : {
|
||||
scroll_speed : 10,
|
||||
scroll_proximity : 20,
|
||||
helper_left : 5,
|
||||
helper_top : 10,
|
||||
threshold : 5,
|
||||
threshold_touch : 10
|
||||
},
|
||||
_trigger : function (event_name, e, data) {
|
||||
if (data === undefined) {
|
||||
data = $.vakata.dnd._get();
|
||||
}
|
||||
data.event = e;
|
||||
$(document).triggerHandler("dnd_" + event_name + ".vakata", data);
|
||||
},
|
||||
_get : function () {
|
||||
return {
|
||||
"data" : vakata_dnd.data,
|
||||
"element" : vakata_dnd.element,
|
||||
"helper" : vakata_dnd.helper
|
||||
};
|
||||
},
|
||||
_clean : function () {
|
||||
if(vakata_dnd.helper) { vakata_dnd.helper.remove(); }
|
||||
if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
|
||||
vakata_dnd = {
|
||||
element : false,
|
||||
target : false,
|
||||
is_down : false,
|
||||
is_drag : false,
|
||||
helper : false,
|
||||
helper_w: 0,
|
||||
data : false,
|
||||
init_x : 0,
|
||||
init_y : 0,
|
||||
scroll_l: 0,
|
||||
scroll_t: 0,
|
||||
scroll_e: false,
|
||||
scroll_i: false,
|
||||
is_touch: false
|
||||
};
|
||||
$(document).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
|
||||
$(document).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
|
||||
},
|
||||
_scroll : function (init_only) {
|
||||
if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) {
|
||||
if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
|
||||
return false;
|
||||
}
|
||||
if(!vakata_dnd.scroll_i) {
|
||||
vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100);
|
||||
return false;
|
||||
}
|
||||
if(init_only === true) { return false; }
|
||||
|
||||
var i = vakata_dnd.scroll_e.scrollTop(),
|
||||
j = vakata_dnd.scroll_e.scrollLeft();
|
||||
vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed);
|
||||
vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed);
|
||||
if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) {
|
||||
/**
|
||||
* triggered on the document when a drag causes an element to scroll
|
||||
* @event
|
||||
* @plugin dnd
|
||||
* @name dnd_scroll.vakata
|
||||
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
|
||||
* @param {DOM} element the DOM element being dragged
|
||||
* @param {jQuery} helper the helper shown next to the mouse
|
||||
* @param {jQuery} event the element that is scrolling
|
||||
*/
|
||||
$.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e);
|
||||
}
|
||||
},
|
||||
start : function (e, data, html) {
|
||||
if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
|
||||
e.pageX = e.originalEvent.changedTouches[0].pageX;
|
||||
e.pageY = e.originalEvent.changedTouches[0].pageY;
|
||||
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
|
||||
}
|
||||
if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); }
|
||||
try {
|
||||
e.currentTarget.unselectable = "on";
|
||||
e.currentTarget.onselectstart = function() { return false; };
|
||||
if(e.currentTarget.style) {
|
||||
e.currentTarget.style.touchAction = "none";
|
||||
e.currentTarget.style.msTouchAction = "none";
|
||||
e.currentTarget.style.MozUserSelect = "none";
|
||||
}
|
||||
} catch(ignore) { }
|
||||
vakata_dnd.init_x = e.pageX;
|
||||
vakata_dnd.init_y = e.pageY;
|
||||
vakata_dnd.data = data;
|
||||
vakata_dnd.is_down = true;
|
||||
vakata_dnd.element = e.currentTarget;
|
||||
vakata_dnd.target = e.target;
|
||||
vakata_dnd.is_touch = e.type === "touchstart";
|
||||
if(html !== false) {
|
||||
vakata_dnd.helper = $("<div id='vakata-dnd'></div>").html(html).css({
|
||||
"display" : "block",
|
||||
"margin" : "0",
|
||||
"padding" : "0",
|
||||
"position" : "absolute",
|
||||
"top" : "-2000px",
|
||||
"lineHeight" : "16px",
|
||||
"zIndex" : "10000"
|
||||
});
|
||||
}
|
||||
$(document).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
|
||||
$(document).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
|
||||
return false;
|
||||
},
|
||||
drag : function (e) {
|
||||
if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
|
||||
e.pageX = e.originalEvent.changedTouches[0].pageX;
|
||||
e.pageY = e.originalEvent.changedTouches[0].pageY;
|
||||
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
|
||||
}
|
||||
if(!vakata_dnd.is_down) { return; }
|
||||
if(!vakata_dnd.is_drag) {
|
||||
if(
|
||||
Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) ||
|
||||
Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold)
|
||||
) {
|
||||
if(vakata_dnd.helper) {
|
||||
vakata_dnd.helper.appendTo(document.body);
|
||||
vakata_dnd.helper_w = vakata_dnd.helper.outerWidth();
|
||||
}
|
||||
vakata_dnd.is_drag = true;
|
||||
$(vakata_dnd.target).one('click.vakata', false);
|
||||
/**
|
||||
* triggered on the document when a drag starts
|
||||
* @event
|
||||
* @plugin dnd
|
||||
* @name dnd_start.vakata
|
||||
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
|
||||
* @param {DOM} element the DOM element being dragged
|
||||
* @param {jQuery} helper the helper shown next to the mouse
|
||||
* @param {Object} event the event that caused the start (probably mousemove)
|
||||
*/
|
||||
$.vakata.dnd._trigger("start", e);
|
||||
}
|
||||
else { return; }
|
||||
}
|
||||
|
||||
var d = false, w = false,
|
||||
dh = false, wh = false,
|
||||
dw = false, ww = false,
|
||||
dt = false, dl = false,
|
||||
ht = false, hl = false;
|
||||
|
||||
vakata_dnd.scroll_t = 0;
|
||||
vakata_dnd.scroll_l = 0;
|
||||
vakata_dnd.scroll_e = false;
|
||||
$($(e.target).parentsUntil("body").addBack().get().reverse())
|
||||
.filter(function () {
|
||||
return (/^auto|scroll$/).test($(this).css("overflow")) &&
|
||||
(this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth);
|
||||
})
|
||||
.each(function () {
|
||||
var t = $(this), o = t.offset();
|
||||
if(this.scrollHeight > this.offsetHeight) {
|
||||
if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
|
||||
if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
|
||||
}
|
||||
if(this.scrollWidth > this.offsetWidth) {
|
||||
if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
|
||||
if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
|
||||
}
|
||||
if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
|
||||
vakata_dnd.scroll_e = $(this);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!vakata_dnd.scroll_e) {
|
||||
d = $(document); w = $(window);
|
||||
dh = d.height(); wh = w.height();
|
||||
dw = d.width(); ww = w.width();
|
||||
dt = d.scrollTop(); dl = d.scrollLeft();
|
||||
if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
|
||||
if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
|
||||
if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
|
||||
if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
|
||||
if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
|
||||
vakata_dnd.scroll_e = d;
|
||||
}
|
||||
}
|
||||
if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); }
|
||||
|
||||
if(vakata_dnd.helper) {
|
||||
ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10);
|
||||
hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10);
|
||||
if(dh && ht + 25 > dh) { ht = dh - 50; }
|
||||
if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); }
|
||||
vakata_dnd.helper.css({
|
||||
left : hl + "px",
|
||||
top : ht + "px"
|
||||
});
|
||||
}
|
||||
/**
|
||||
* triggered on the document when a drag is in progress
|
||||
* @event
|
||||
* @plugin dnd
|
||||
* @name dnd_move.vakata
|
||||
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
|
||||
* @param {DOM} element the DOM element being dragged
|
||||
* @param {jQuery} helper the helper shown next to the mouse
|
||||
* @param {Object} event the event that caused this to trigger (most likely mousemove)
|
||||
*/
|
||||
$.vakata.dnd._trigger("move", e);
|
||||
return false;
|
||||
},
|
||||
stop : function (e) {
|
||||
if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
|
||||
e.pageX = e.originalEvent.changedTouches[0].pageX;
|
||||
e.pageY = e.originalEvent.changedTouches[0].pageY;
|
||||
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
|
||||
}
|
||||
if(vakata_dnd.is_drag) {
|
||||
/**
|
||||
* triggered on the document when a drag stops (the dragged element is dropped)
|
||||
* @event
|
||||
* @plugin dnd
|
||||
* @name dnd_stop.vakata
|
||||
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
|
||||
* @param {DOM} element the DOM element being dragged
|
||||
* @param {jQuery} helper the helper shown next to the mouse
|
||||
* @param {Object} event the event that caused the stop
|
||||
*/
|
||||
if (e.target !== vakata_dnd.target) {
|
||||
$(vakata_dnd.target).off('click.vakata');
|
||||
}
|
||||
$.vakata.dnd._trigger("stop", e);
|
||||
}
|
||||
else {
|
||||
if(e.type === "touchend" && e.target === vakata_dnd.target) {
|
||||
var to = setTimeout(function () { $(e.target).click(); }, 100);
|
||||
$(e.target).one('click', function() { if(to) { clearTimeout(to); } });
|
||||
}
|
||||
}
|
||||
$.vakata.dnd._clean();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}($));
|
||||
|
||||
// include the dnd plugin by default
|
||||
// $.jstree.defaults.plugins.push("dnd");
|
||||
}));
|
4924
app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.js
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* ### Massload plugin
|
||||
*
|
||||
* Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading).
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.massload', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.massload) { return; }
|
||||
|
||||
/**
|
||||
* massload configuration
|
||||
*
|
||||
* It is possible to set this to a standard jQuery-like AJAX config.
|
||||
* In addition to the standard jQuery ajax options here you can supply functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node IDs need to be loaded, the return value of those functions will be used.
|
||||
*
|
||||
* You can also set this to a function, that function will receive the node IDs being loaded as argument and a second param which is a function (callback) which should be called with the result.
|
||||
*
|
||||
* Both the AJAX and the function approach rely on the same return value - an object where the keys are the node IDs, and the value is the children of that node as an array.
|
||||
*
|
||||
* {
|
||||
* "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }],
|
||||
* "id2" : [{ "text" : "Child of ID2", "id" : "c3" }]
|
||||
* }
|
||||
*
|
||||
* @name $.jstree.defaults.massload
|
||||
* @plugin massload
|
||||
*/
|
||||
$.jstree.defaults.massload = null;
|
||||
$.jstree.plugins.massload = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
this._data.massload = {};
|
||||
parent.init.call(this, el, options);
|
||||
};
|
||||
this._load_nodes = function (nodes, callback, is_callback, force_reload) {
|
||||
var s = this.settings.massload,
|
||||
nodesString = JSON.stringify(nodes),
|
||||
toLoad = [],
|
||||
m = this._model.data,
|
||||
i, j, dom;
|
||||
if (!is_callback) {
|
||||
for(i = 0, j = nodes.length; i < j; i++) {
|
||||
if(!m[nodes[i]] || ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || force_reload) ) {
|
||||
toLoad.push(nodes[i]);
|
||||
dom = this.get_node(nodes[i], true);
|
||||
if (dom && dom.length) {
|
||||
dom.addClass("jstree-loading").attr('aria-busy',true);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._data.massload = {};
|
||||
if (toLoad.length) {
|
||||
if($.isFunction(s)) {
|
||||
return s.call(this, toLoad, $.proxy(function (data) {
|
||||
var i, j;
|
||||
if(data) {
|
||||
for(i in data) {
|
||||
if(data.hasOwnProperty(i)) {
|
||||
this._data.massload[i] = data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
for(i = 0, j = nodes.length; i < j; i++) {
|
||||
dom = this.get_node(nodes[i], true);
|
||||
if (dom && dom.length) {
|
||||
dom.removeClass("jstree-loading").attr('aria-busy',false);
|
||||
}
|
||||
}
|
||||
parent._load_nodes.call(this, nodes, callback, is_callback, force_reload);
|
||||
}, this));
|
||||
}
|
||||
if(typeof s === 'object' && s && s.url) {
|
||||
s = $.extend(true, {}, s);
|
||||
if($.isFunction(s.url)) {
|
||||
s.url = s.url.call(this, toLoad);
|
||||
}
|
||||
if($.isFunction(s.data)) {
|
||||
s.data = s.data.call(this, toLoad);
|
||||
}
|
||||
return $.ajax(s)
|
||||
.done($.proxy(function (data,t,x) {
|
||||
var i, j;
|
||||
if(data) {
|
||||
for(i in data) {
|
||||
if(data.hasOwnProperty(i)) {
|
||||
this._data.massload[i] = data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
for(i = 0, j = nodes.length; i < j; i++) {
|
||||
dom = this.get_node(nodes[i], true);
|
||||
if (dom && dom.length) {
|
||||
dom.removeClass("jstree-loading").attr('aria-busy',false);
|
||||
}
|
||||
}
|
||||
parent._load_nodes.call(this, nodes, callback, is_callback, force_reload);
|
||||
}, this))
|
||||
.fail($.proxy(function (f) {
|
||||
parent._load_nodes.call(this, nodes, callback, is_callback, force_reload);
|
||||
}, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
return parent._load_nodes.call(this, nodes, callback, is_callback, force_reload);
|
||||
};
|
||||
this._load_node = function (obj, callback) {
|
||||
var data = this._data.massload[obj.id],
|
||||
rslt = null, dom;
|
||||
if(data) {
|
||||
rslt = this[typeof data === 'string' ? '_append_html_data' : '_append_json_data'](
|
||||
obj,
|
||||
typeof data === 'string' ? $($.parseHTML(data)).filter(function () { return this.nodeType !== 3; }) : data,
|
||||
function (status) { callback.call(this, status); }
|
||||
);
|
||||
dom = this.get_node(obj.id, true);
|
||||
if (dom && dom.length) {
|
||||
dom.removeClass("jstree-loading").attr('aria-busy',false);
|
||||
}
|
||||
delete this._data.massload[obj.id];
|
||||
return rslt;
|
||||
}
|
||||
return parent._load_node.call(this, obj, callback);
|
||||
};
|
||||
};
|
||||
}));
|
@ -0,0 +1,421 @@
|
||||
/**
|
||||
* ### Search plugin
|
||||
*
|
||||
* Adds search functionality to jsTree.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.search', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.search) { return; }
|
||||
|
||||
/**
|
||||
* stores all defaults for the search plugin
|
||||
* @name $.jstree.defaults.search
|
||||
* @plugin search
|
||||
*/
|
||||
$.jstree.defaults.search = {
|
||||
/**
|
||||
* a jQuery-like AJAX config, which jstree uses if a server should be queried for results.
|
||||
*
|
||||
* A `str` (which is the search string) parameter will be added with the request, an optional `inside` parameter will be added if the search is limited to a node id. The expected result is a JSON array with nodes that need to be opened so that matching nodes will be revealed.
|
||||
* Leave this setting as `false` to not query the server. You can also set this to a function, which will be invoked in the instance's scope and receive 3 parameters - the search string, the callback to call with the array of nodes to load, and the optional node ID to limit the search to
|
||||
* @name $.jstree.defaults.search.ajax
|
||||
* @plugin search
|
||||
*/
|
||||
ajax : false,
|
||||
/**
|
||||
* Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`.
|
||||
* @name $.jstree.defaults.search.fuzzy
|
||||
* @plugin search
|
||||
*/
|
||||
fuzzy : false,
|
||||
/**
|
||||
* Indicates if the search should be case sensitive. Default is `false`.
|
||||
* @name $.jstree.defaults.search.case_sensitive
|
||||
* @plugin search
|
||||
*/
|
||||
case_sensitive : false,
|
||||
/**
|
||||
* Indicates if the tree should be filtered (by default) to show only matching nodes (keep in mind this can be a heavy on large trees in old browsers).
|
||||
* This setting can be changed at runtime when calling the search method. Default is `false`.
|
||||
* @name $.jstree.defaults.search.show_only_matches
|
||||
* @plugin search
|
||||
*/
|
||||
show_only_matches : false,
|
||||
/**
|
||||
* Indicates if the children of matched element are shown (when show_only_matches is true)
|
||||
* This setting can be changed at runtime when calling the search method. Default is `false`.
|
||||
* @name $.jstree.defaults.search.show_only_matches_children
|
||||
* @plugin search
|
||||
*/
|
||||
show_only_matches_children : false,
|
||||
/**
|
||||
* Indicates if all nodes opened to reveal the search result, should be closed when the search is cleared or a new search is performed. Default is `true`.
|
||||
* @name $.jstree.defaults.search.close_opened_onclear
|
||||
* @plugin search
|
||||
*/
|
||||
close_opened_onclear : true,
|
||||
/**
|
||||
* Indicates if only leaf nodes should be included in search results. Default is `false`.
|
||||
* @name $.jstree.defaults.search.search_leaves_only
|
||||
* @plugin search
|
||||
*/
|
||||
search_leaves_only : false,
|
||||
/**
|
||||
* If set to a function it wil be called in the instance's scope with two arguments - search string and node (where node will be every node in the structure, so use with caution).
|
||||
* If the function returns a truthy value the node will be considered a match (it might not be displayed if search_only_leaves is set to true and the node is not a leaf). Default is `false`.
|
||||
* @name $.jstree.defaults.search.search_callback
|
||||
* @plugin search
|
||||
*/
|
||||
search_callback : false
|
||||
};
|
||||
|
||||
$.jstree.plugins.search = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
|
||||
this._data.search.str = "";
|
||||
this._data.search.dom = $();
|
||||
this._data.search.res = [];
|
||||
this._data.search.opn = [];
|
||||
this._data.search.som = false;
|
||||
this._data.search.smc = false;
|
||||
this._data.search.hdn = [];
|
||||
|
||||
this.element
|
||||
.on("search.jstree", $.proxy(function (e, data) {
|
||||
if(this._data.search.som && data.res.length) {
|
||||
var m = this._model.data, i, j, p = [], k, l;
|
||||
for(i = 0, j = data.res.length; i < j; i++) {
|
||||
if(m[data.res[i]] && !m[data.res[i]].state.hidden) {
|
||||
p.push(data.res[i]);
|
||||
p = p.concat(m[data.res[i]].parents);
|
||||
if(this._data.search.smc) {
|
||||
for (k = 0, l = m[data.res[i]].children_d.length; k < l; k++) {
|
||||
if (m[m[data.res[i]].children_d[k]] && !m[m[data.res[i]].children_d[k]].state.hidden) {
|
||||
p.push(m[data.res[i]].children_d[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p = $.vakata.array_remove_item($.vakata.array_unique(p), $.jstree.root);
|
||||
this._data.search.hdn = this.hide_all(true);
|
||||
this.show_node(p, true);
|
||||
this.redraw(true);
|
||||
}
|
||||
}, this))
|
||||
.on("clear_search.jstree", $.proxy(function (e, data) {
|
||||
if(this._data.search.som && data.res.length) {
|
||||
this.show_node(this._data.search.hdn, true);
|
||||
this.redraw(true);
|
||||
}
|
||||
}, this));
|
||||
};
|
||||
/**
|
||||
* used to search the tree nodes for a given string
|
||||
* @name search(str [, skip_async])
|
||||
* @param {String} str the search string
|
||||
* @param {Boolean} skip_async if set to true server will not be queried even if configured
|
||||
* @param {Boolean} show_only_matches if set to true only matching nodes will be shown (keep in mind this can be very slow on large trees or old browsers)
|
||||
* @param {mixed} inside an optional node to whose children to limit the search
|
||||
* @param {Boolean} append if set to true the results of this search are appended to the previous search
|
||||
* @plugin search
|
||||
* @trigger search.jstree
|
||||
*/
|
||||
this.search = function (str, skip_async, show_only_matches, inside, append, show_only_matches_children) {
|
||||
if(str === false || $.trim(str.toString()) === "") {
|
||||
return this.clear_search();
|
||||
}
|
||||
inside = this.get_node(inside);
|
||||
inside = inside && inside.id ? inside.id : null;
|
||||
str = str.toString();
|
||||
var s = this.settings.search,
|
||||
a = s.ajax ? s.ajax : false,
|
||||
m = this._model.data,
|
||||
f = null,
|
||||
r = [],
|
||||
p = [], i, j;
|
||||
if(this._data.search.res.length && !append) {
|
||||
this.clear_search();
|
||||
}
|
||||
if(show_only_matches === undefined) {
|
||||
show_only_matches = s.show_only_matches;
|
||||
}
|
||||
if(show_only_matches_children === undefined) {
|
||||
show_only_matches_children = s.show_only_matches_children;
|
||||
}
|
||||
if(!skip_async && a !== false) {
|
||||
if($.isFunction(a)) {
|
||||
return a.call(this, str, $.proxy(function (d) {
|
||||
if(d && d.d) { d = d.d; }
|
||||
this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
|
||||
this.search(str, true, show_only_matches, inside, append, show_only_matches_children);
|
||||
});
|
||||
}, this), inside);
|
||||
}
|
||||
else {
|
||||
a = $.extend({}, a);
|
||||
if(!a.data) { a.data = {}; }
|
||||
a.data.str = str;
|
||||
if(inside) {
|
||||
a.data.inside = inside;
|
||||
}
|
||||
if (this._data.search.lastRequest) {
|
||||
this._data.search.lastRequest.abort();
|
||||
}
|
||||
this._data.search.lastRequest = $.ajax(a)
|
||||
.fail($.proxy(function () {
|
||||
this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON.stringify(a) };
|
||||
this.settings.core.error.call(this, this._data.core.last_error);
|
||||
}, this))
|
||||
.done($.proxy(function (d) {
|
||||
if(d && d.d) { d = d.d; }
|
||||
this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
|
||||
this.search(str, true, show_only_matches, inside, append, show_only_matches_children);
|
||||
});
|
||||
}, this));
|
||||
return this._data.search.lastRequest;
|
||||
}
|
||||
}
|
||||
if(!append) {
|
||||
this._data.search.str = str;
|
||||
this._data.search.dom = $();
|
||||
this._data.search.res = [];
|
||||
this._data.search.opn = [];
|
||||
this._data.search.som = show_only_matches;
|
||||
this._data.search.smc = show_only_matches_children;
|
||||
}
|
||||
|
||||
f = new $.vakata.search(str, true, { caseSensitive : s.case_sensitive, fuzzy : s.fuzzy });
|
||||
$.each(m[inside ? inside : $.jstree.root].children_d, function (ii, i) {
|
||||
var v = m[i];
|
||||
if(v.text && !v.state.hidden && (!s.search_leaves_only || (v.state.loaded && v.children.length === 0)) && ( (s.search_callback && s.search_callback.call(this, str, v)) || (!s.search_callback && f.search(v.text).isMatch) ) ) {
|
||||
r.push(i);
|
||||
p = p.concat(v.parents);
|
||||
}
|
||||
});
|
||||
if(r.length) {
|
||||
p = $.vakata.array_unique(p);
|
||||
for(i = 0, j = p.length; i < j; i++) {
|
||||
if(p[i] !== $.jstree.root && m[p[i]] && this.open_node(p[i], null, 0) === true) {
|
||||
this._data.search.opn.push(p[i]);
|
||||
}
|
||||
}
|
||||
if(!append) {
|
||||
this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #')));
|
||||
this._data.search.res = r;
|
||||
}
|
||||
else {
|
||||
this._data.search.dom = this._data.search.dom.add($(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))));
|
||||
this._data.search.res = $.vakata.array_unique(this._data.search.res.concat(r));
|
||||
}
|
||||
this._data.search.dom.children(".jstree-anchor").addClass('jstree-search');
|
||||
}
|
||||
/**
|
||||
* triggered after search is complete
|
||||
* @event
|
||||
* @name search.jstree
|
||||
* @param {jQuery} nodes a jQuery collection of matching nodes
|
||||
* @param {String} str the search string
|
||||
* @param {Array} res a collection of objects represeing the matching nodes
|
||||
* @plugin search
|
||||
*/
|
||||
this.trigger('search', { nodes : this._data.search.dom, str : str, res : this._data.search.res, show_only_matches : show_only_matches });
|
||||
};
|
||||
/**
|
||||
* used to clear the last search (removes classes and shows all nodes if filtering is on)
|
||||
* @name clear_search()
|
||||
* @plugin search
|
||||
* @trigger clear_search.jstree
|
||||
*/
|
||||
this.clear_search = function () {
|
||||
if(this.settings.search.close_opened_onclear) {
|
||||
this.close_node(this._data.search.opn, 0);
|
||||
}
|
||||
/**
|
||||
* triggered after search is complete
|
||||
* @event
|
||||
* @name clear_search.jstree
|
||||
* @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search)
|
||||
* @param {String} str the search string (the last search string)
|
||||
* @param {Array} res a collection of objects represeing the matching nodes (the result from the last search)
|
||||
* @plugin search
|
||||
*/
|
||||
this.trigger('clear_search', { 'nodes' : this._data.search.dom, str : this._data.search.str, res : this._data.search.res });
|
||||
if(this._data.search.res.length) {
|
||||
this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(this._data.search.res, function (v) {
|
||||
return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&');
|
||||
}).join(', #')));
|
||||
this._data.search.dom.children(".jstree-anchor").removeClass("jstree-search");
|
||||
}
|
||||
this._data.search.str = "";
|
||||
this._data.search.res = [];
|
||||
this._data.search.opn = [];
|
||||
this._data.search.dom = $();
|
||||
};
|
||||
|
||||
this.redraw_node = function(obj, deep, callback, force_render) {
|
||||
obj = parent.redraw_node.apply(this, arguments);
|
||||
if(obj) {
|
||||
if($.inArray(obj.id, this._data.search.res) !== -1) {
|
||||
var i, j, tmp = null;
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
tmp.className += ' jstree-search';
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
|
||||
// helpers
|
||||
(function ($) {
|
||||
// from http://kiro.me/projects/fuse.html
|
||||
$.vakata.search = function(pattern, txt, options) {
|
||||
options = options || {};
|
||||
options = $.extend({}, $.vakata.search.defaults, options);
|
||||
if(options.fuzzy !== false) {
|
||||
options.fuzzy = true;
|
||||
}
|
||||
pattern = options.caseSensitive ? pattern : pattern.toLowerCase();
|
||||
var MATCH_LOCATION = options.location,
|
||||
MATCH_DISTANCE = options.distance,
|
||||
MATCH_THRESHOLD = options.threshold,
|
||||
patternLen = pattern.length,
|
||||
matchmask, pattern_alphabet, match_bitapScore, search;
|
||||
if(patternLen > 32) {
|
||||
options.fuzzy = false;
|
||||
}
|
||||
if(options.fuzzy) {
|
||||
matchmask = 1 << (patternLen - 1);
|
||||
pattern_alphabet = (function () {
|
||||
var mask = {},
|
||||
i = 0;
|
||||
for (i = 0; i < patternLen; i++) {
|
||||
mask[pattern.charAt(i)] = 0;
|
||||
}
|
||||
for (i = 0; i < patternLen; i++) {
|
||||
mask[pattern.charAt(i)] |= 1 << (patternLen - i - 1);
|
||||
}
|
||||
return mask;
|
||||
}());
|
||||
match_bitapScore = function (e, x) {
|
||||
var accuracy = e / patternLen,
|
||||
proximity = Math.abs(MATCH_LOCATION - x);
|
||||
if(!MATCH_DISTANCE) {
|
||||
return proximity ? 1.0 : accuracy;
|
||||
}
|
||||
return accuracy + (proximity / MATCH_DISTANCE);
|
||||
};
|
||||
}
|
||||
search = function (text) {
|
||||
text = options.caseSensitive ? text : text.toLowerCase();
|
||||
if(pattern === text || text.indexOf(pattern) !== -1) {
|
||||
return {
|
||||
isMatch: true,
|
||||
score: 0
|
||||
};
|
||||
}
|
||||
if(!options.fuzzy) {
|
||||
return {
|
||||
isMatch: false,
|
||||
score: 1
|
||||
};
|
||||
}
|
||||
var i, j,
|
||||
textLen = text.length,
|
||||
scoreThreshold = MATCH_THRESHOLD,
|
||||
bestLoc = text.indexOf(pattern, MATCH_LOCATION),
|
||||
binMin, binMid,
|
||||
binMax = patternLen + textLen,
|
||||
lastRd, start, finish, rd, charMatch,
|
||||
score = 1,
|
||||
locations = [];
|
||||
if (bestLoc !== -1) {
|
||||
scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
|
||||
bestLoc = text.lastIndexOf(pattern, MATCH_LOCATION + patternLen);
|
||||
if (bestLoc !== -1) {
|
||||
scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
|
||||
}
|
||||
}
|
||||
bestLoc = -1;
|
||||
for (i = 0; i < patternLen; i++) {
|
||||
binMin = 0;
|
||||
binMid = binMax;
|
||||
while (binMin < binMid) {
|
||||
if (match_bitapScore(i, MATCH_LOCATION + binMid) <= scoreThreshold) {
|
||||
binMin = binMid;
|
||||
} else {
|
||||
binMax = binMid;
|
||||
}
|
||||
binMid = Math.floor((binMax - binMin) / 2 + binMin);
|
||||
}
|
||||
binMax = binMid;
|
||||
start = Math.max(1, MATCH_LOCATION - binMid + 1);
|
||||
finish = Math.min(MATCH_LOCATION + binMid, textLen) + patternLen;
|
||||
rd = new Array(finish + 2);
|
||||
rd[finish + 1] = (1 << i) - 1;
|
||||
for (j = finish; j >= start; j--) {
|
||||
charMatch = pattern_alphabet[text.charAt(j - 1)];
|
||||
if (i === 0) {
|
||||
rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
|
||||
} else {
|
||||
rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1];
|
||||
}
|
||||
if (rd[j] & matchmask) {
|
||||
score = match_bitapScore(i, j - 1);
|
||||
if (score <= scoreThreshold) {
|
||||
scoreThreshold = score;
|
||||
bestLoc = j - 1;
|
||||
locations.push(bestLoc);
|
||||
if (bestLoc > MATCH_LOCATION) {
|
||||
start = Math.max(1, 2 * MATCH_LOCATION - bestLoc);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) {
|
||||
break;
|
||||
}
|
||||
lastRd = rd;
|
||||
}
|
||||
return {
|
||||
isMatch: bestLoc >= 0,
|
||||
score: score
|
||||
};
|
||||
};
|
||||
return txt === true ? { 'search' : search } : search(txt);
|
||||
};
|
||||
$.vakata.search.defaults = {
|
||||
location : 0,
|
||||
distance : 100,
|
||||
threshold : 0.6,
|
||||
fuzzy : false,
|
||||
caseSensitive : false
|
||||
};
|
||||
}($));
|
||||
|
||||
// include the search plugin by default
|
||||
// $.jstree.defaults.plugins.push("search");
|
||||
}));
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* ### Sort plugin
|
||||
*
|
||||
* Automatically sorts all siblings in the tree according to a sorting function.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.sort', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.sort) { return; }
|
||||
|
||||
/**
|
||||
* the settings function used to sort the nodes.
|
||||
* It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`.
|
||||
* @name $.jstree.defaults.sort
|
||||
* @plugin sort
|
||||
*/
|
||||
$.jstree.defaults.sort = function (a, b) {
|
||||
//return this.get_type(a) === this.get_type(b) ? (this.get_text(a) > this.get_text(b) ? 1 : -1) : this.get_type(a) >= this.get_type(b);
|
||||
return this.get_text(a) > this.get_text(b) ? 1 : -1;
|
||||
};
|
||||
$.jstree.plugins.sort = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
this.element
|
||||
.on("model.jstree", $.proxy(function (e, data) {
|
||||
this.sort(data.parent, true);
|
||||
}, this))
|
||||
.on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) {
|
||||
this.sort(data.parent || data.node.parent, false);
|
||||
this.redraw_node(data.parent || data.node.parent, true);
|
||||
}, this))
|
||||
.on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) {
|
||||
this.sort(data.parent, false);
|
||||
this.redraw_node(data.parent, true);
|
||||
}, this));
|
||||
};
|
||||
/**
|
||||
* used to sort a node's children
|
||||
* @private
|
||||
* @name sort(obj [, deep])
|
||||
* @param {mixed} obj the node
|
||||
* @param {Boolean} deep if set to `true` nodes are sorted recursively.
|
||||
* @plugin sort
|
||||
* @trigger search.jstree
|
||||
*/
|
||||
this.sort = function (obj, deep) {
|
||||
var i, j;
|
||||
obj = this.get_node(obj);
|
||||
if(obj && obj.children && obj.children.length) {
|
||||
obj.children.sort($.proxy(this.settings.sort, this));
|
||||
if(deep) {
|
||||
for(i = 0, j = obj.children_d.length; i < j; i++) {
|
||||
this.sort(obj.children_d[i], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// include the sort plugin by default
|
||||
// $.jstree.defaults.plugins.push("sort");
|
||||
}));
|
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* ### State plugin
|
||||
*
|
||||
* Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc)
|
||||
*/
|
||||
/*globals jQuery, define, exports, require */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.state', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.state) { return; }
|
||||
|
||||
var to = false;
|
||||
/**
|
||||
* stores all defaults for the state plugin
|
||||
* @name $.jstree.defaults.state
|
||||
* @plugin state
|
||||
*/
|
||||
$.jstree.defaults.state = {
|
||||
/**
|
||||
* A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`.
|
||||
* @name $.jstree.defaults.state.key
|
||||
* @plugin state
|
||||
*/
|
||||
key : 'jstree',
|
||||
/**
|
||||
* A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`.
|
||||
* @name $.jstree.defaults.state.events
|
||||
* @plugin state
|
||||
*/
|
||||
events : 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree',
|
||||
/**
|
||||
* Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire.
|
||||
* @name $.jstree.defaults.state.ttl
|
||||
* @plugin state
|
||||
*/
|
||||
ttl : false,
|
||||
/**
|
||||
* A function that will be executed prior to restoring state with one argument - the state object. Can be used to clear unwanted parts of the state.
|
||||
* @name $.jstree.defaults.state.filter
|
||||
* @plugin state
|
||||
*/
|
||||
filter : false,
|
||||
/**
|
||||
* Should loaded nodes be restored (setting this to true means that it is possible that the whole tree will be loaded for some users - use with caution). Defaults to `false`
|
||||
* @name $.jstree.defaults.state.preserve_loaded
|
||||
* @plugin state
|
||||
*/
|
||||
preserve_loaded : false
|
||||
};
|
||||
$.jstree.plugins.state = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
var bind = $.proxy(function () {
|
||||
this.element.on(this.settings.state.events, $.proxy(function () {
|
||||
if(to) { clearTimeout(to); }
|
||||
to = setTimeout($.proxy(function () { this.save_state(); }, this), 100);
|
||||
}, this));
|
||||
/**
|
||||
* triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore).
|
||||
* @event
|
||||
* @name state_ready.jstree
|
||||
* @plugin state
|
||||
*/
|
||||
this.trigger('state_ready');
|
||||
}, this);
|
||||
this.element
|
||||
.on("ready.jstree", $.proxy(function (e, data) {
|
||||
this.element.one("restore_state.jstree", bind);
|
||||
if(!this.restore_state()) { bind(); }
|
||||
}, this));
|
||||
};
|
||||
/**
|
||||
* save the state
|
||||
* @name save_state()
|
||||
* @plugin state
|
||||
*/
|
||||
this.save_state = function () {
|
||||
var tm = this.get_state();
|
||||
if (!this.settings.state.preserve_loaded) {
|
||||
delete tm.core.loaded;
|
||||
}
|
||||
var st = { 'state' : tm, 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) };
|
||||
$.vakata.storage.set(this.settings.state.key, JSON.stringify(st));
|
||||
};
|
||||
/**
|
||||
* restore the state from the user's computer
|
||||
* @name restore_state()
|
||||
* @plugin state
|
||||
*/
|
||||
this.restore_state = function () {
|
||||
var k = $.vakata.storage.get(this.settings.state.key);
|
||||
if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } }
|
||||
if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; }
|
||||
if(!!k && k.state) { k = k.state; }
|
||||
if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); }
|
||||
if(!!k) {
|
||||
if (!this.settings.state.preserve_loaded) {
|
||||
delete k.core.loaded;
|
||||
}
|
||||
this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); });
|
||||
this.set_state(k);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* clear the state on the user's computer
|
||||
* @name clear_state()
|
||||
* @plugin state
|
||||
*/
|
||||
this.clear_state = function () {
|
||||
return $.vakata.storage.del(this.settings.state.key);
|
||||
};
|
||||
};
|
||||
|
||||
(function ($, undefined) {
|
||||
$.vakata.storage = {
|
||||
// simply specifying the functions in FF throws an error
|
||||
set : function (key, val) { return window.localStorage.setItem(key, val); },
|
||||
get : function (key) { return window.localStorage.getItem(key); },
|
||||
del : function (key) { return window.localStorage.removeItem(key); }
|
||||
};
|
||||
}($));
|
||||
|
||||
// include the state plugin by default
|
||||
// $.jstree.defaults.plugins.push("state");
|
||||
}));
|
@ -0,0 +1,372 @@
|
||||
/**
|
||||
* ### Types plugin
|
||||
*
|
||||
* Makes it possible to add predefined types for groups of nodes, which make it possible to easily control nesting rules and icon for each group.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.types', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.types) { return; }
|
||||
|
||||
/**
|
||||
* An object storing all types as key value pairs, where the key is the type name and the value is an object that could contain following keys (all optional).
|
||||
*
|
||||
* * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited.
|
||||
* * `max_depth` the maximum number of nesting this node type can have. A value of `1` would mean that the node can have children, but no grandchildren. Do not specify or set to `-1` for unlimited.
|
||||
* * `valid_children` an array of node type strings, that nodes of this type can have as children. Do not specify or set to `-1` for no limits.
|
||||
* * `icon` a string - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class. Omit to use the default icon from your theme.
|
||||
* * `li_attr` an object of values which will be used to add HTML attributes on the resulting LI DOM node (merged with the node's own data)
|
||||
* * `a_attr` an object of values which will be used to add HTML attributes on the resulting A DOM node (merged with the node's own data)
|
||||
*
|
||||
* There are two predefined types:
|
||||
*
|
||||
* * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes.
|
||||
* * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified.
|
||||
*
|
||||
* @name $.jstree.defaults.types
|
||||
* @plugin types
|
||||
*/
|
||||
$.jstree.defaults.types = {
|
||||
'default' : {}
|
||||
};
|
||||
$.jstree.defaults.types[$.jstree.root] = {};
|
||||
|
||||
$.jstree.plugins.types = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
var i, j;
|
||||
if(options && options.types && options.types['default']) {
|
||||
for(i in options.types) {
|
||||
if(i !== "default" && i !== $.jstree.root && options.types.hasOwnProperty(i)) {
|
||||
for(j in options.types['default']) {
|
||||
if(options.types['default'].hasOwnProperty(j) && options.types[i][j] === undefined) {
|
||||
options.types[i][j] = options.types['default'][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parent.init.call(this, el, options);
|
||||
this._model.data[$.jstree.root].type = $.jstree.root;
|
||||
};
|
||||
this.refresh = function (skip_loading, forget_state) {
|
||||
parent.refresh.call(this, skip_loading, forget_state);
|
||||
this._model.data[$.jstree.root].type = $.jstree.root;
|
||||
};
|
||||
this.bind = function () {
|
||||
this.element
|
||||
.on('model.jstree', $.proxy(function (e, data) {
|
||||
var m = this._model.data,
|
||||
dpc = data.nodes,
|
||||
t = this.settings.types,
|
||||
i, j, c = 'default', k;
|
||||
for(i = 0, j = dpc.length; i < j; i++) {
|
||||
c = 'default';
|
||||
if(m[dpc[i]].original && m[dpc[i]].original.type && t[m[dpc[i]].original.type]) {
|
||||
c = m[dpc[i]].original.type;
|
||||
}
|
||||
if(m[dpc[i]].data && m[dpc[i]].data.jstree && m[dpc[i]].data.jstree.type && t[m[dpc[i]].data.jstree.type]) {
|
||||
c = m[dpc[i]].data.jstree.type;
|
||||
}
|
||||
m[dpc[i]].type = c;
|
||||
if(m[dpc[i]].icon === true && t[c].icon !== undefined) {
|
||||
m[dpc[i]].icon = t[c].icon;
|
||||
}
|
||||
if(t[c].li_attr !== undefined && typeof t[c].li_attr === 'object') {
|
||||
for (k in t[c].li_attr) {
|
||||
if (t[c].li_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (m[dpc[i]].li_attr[k] === undefined) {
|
||||
m[dpc[i]].li_attr[k] = t[c].li_attr[k];
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[dpc[i]].li_attr['class'] = t[c].li_attr['class'] + ' ' + m[dpc[i]].li_attr['class'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(t[c].a_attr !== undefined && typeof t[c].a_attr === 'object') {
|
||||
for (k in t[c].a_attr) {
|
||||
if (t[c].a_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (m[dpc[i]].a_attr[k] === undefined) {
|
||||
m[dpc[i]].a_attr[k] = t[c].a_attr[k];
|
||||
}
|
||||
else if (k === 'href' && m[dpc[i]].a_attr[k] === '#') {
|
||||
m[dpc[i]].a_attr['href'] = t[c].a_attr['href'];
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[dpc[i]].a_attr['class'] = t[c].a_attr['class'] + ' ' + m[dpc[i]].a_attr['class'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m[$.jstree.root].type = $.jstree.root;
|
||||
}, this));
|
||||
parent.bind.call(this);
|
||||
};
|
||||
this.get_json = function (obj, options, flat) {
|
||||
var i, j,
|
||||
m = this._model.data,
|
||||
opt = options ? $.extend(true, {}, options, {no_id:false}) : {},
|
||||
tmp = parent.get_json.call(this, obj, opt, flat);
|
||||
if(tmp === false) { return false; }
|
||||
if($.isArray(tmp)) {
|
||||
for(i = 0, j = tmp.length; i < j; i++) {
|
||||
tmp[i].type = tmp[i].id && m[tmp[i].id] && m[tmp[i].id].type ? m[tmp[i].id].type : "default";
|
||||
if(options && options.no_id) {
|
||||
delete tmp[i].id;
|
||||
if(tmp[i].li_attr && tmp[i].li_attr.id) {
|
||||
delete tmp[i].li_attr.id;
|
||||
}
|
||||
if(tmp[i].a_attr && tmp[i].a_attr.id) {
|
||||
delete tmp[i].a_attr.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
tmp.type = tmp.id && m[tmp.id] && m[tmp.id].type ? m[tmp.id].type : "default";
|
||||
if(options && options.no_id) {
|
||||
tmp = this._delete_ids(tmp);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
};
|
||||
this._delete_ids = function (tmp) {
|
||||
if($.isArray(tmp)) {
|
||||
for(var i = 0, j = tmp.length; i < j; i++) {
|
||||
tmp[i] = this._delete_ids(tmp[i]);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
delete tmp.id;
|
||||
if(tmp.li_attr && tmp.li_attr.id) {
|
||||
delete tmp.li_attr.id;
|
||||
}
|
||||
if(tmp.a_attr && tmp.a_attr.id) {
|
||||
delete tmp.a_attr.id;
|
||||
}
|
||||
if(tmp.children && $.isArray(tmp.children)) {
|
||||
tmp.children = this._delete_ids(tmp.children);
|
||||
}
|
||||
return tmp;
|
||||
};
|
||||
this.check = function (chk, obj, par, pos, more) {
|
||||
if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
|
||||
obj = obj && obj.id ? obj : this.get_node(obj);
|
||||
par = par && par.id ? par : this.get_node(par);
|
||||
var m = obj && obj.id ? (more && more.origin ? more.origin : $.jstree.reference(obj.id)) : null, tmp, d, i, j;
|
||||
m = m && m._model && m._model.data ? m._model.data : null;
|
||||
switch(chk) {
|
||||
case "create_node":
|
||||
case "move_node":
|
||||
case "copy_node":
|
||||
if(chk !== 'move_node' || $.inArray(obj.id, par.children) === -1) {
|
||||
tmp = this.get_rules(par);
|
||||
if(tmp.max_children !== undefined && tmp.max_children !== -1 && tmp.max_children === par.children.length) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_01', 'reason' : 'max_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
return false;
|
||||
}
|
||||
if(tmp.valid_children !== undefined && tmp.valid_children !== -1 && $.inArray((obj.type || 'default'), tmp.valid_children) === -1) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_02', 'reason' : 'valid_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
return false;
|
||||
}
|
||||
if(m && obj.children_d && obj.parents) {
|
||||
d = 0;
|
||||
for(i = 0, j = obj.children_d.length; i < j; i++) {
|
||||
d = Math.max(d, m[obj.children_d[i]].parents.length);
|
||||
}
|
||||
d = d - obj.parents.length + 1;
|
||||
}
|
||||
if(d <= 0 || d === undefined) { d = 1; }
|
||||
do {
|
||||
if(tmp.max_depth !== undefined && tmp.max_depth !== -1 && tmp.max_depth < d) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_03', 'reason' : 'max_depth prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
return false;
|
||||
}
|
||||
par = this.get_node(par.parent);
|
||||
tmp = this.get_rules(par);
|
||||
d++;
|
||||
} while(par);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/**
|
||||
* used to retrieve the type settings object for a node
|
||||
* @name get_rules(obj)
|
||||
* @param {mixed} obj the node to find the rules for
|
||||
* @return {Object}
|
||||
* @plugin types
|
||||
*/
|
||||
this.get_rules = function (obj) {
|
||||
obj = this.get_node(obj);
|
||||
if(!obj) { return false; }
|
||||
var tmp = this.get_type(obj, true);
|
||||
if(tmp.max_depth === undefined) { tmp.max_depth = -1; }
|
||||
if(tmp.max_children === undefined) { tmp.max_children = -1; }
|
||||
if(tmp.valid_children === undefined) { tmp.valid_children = -1; }
|
||||
return tmp;
|
||||
};
|
||||
/**
|
||||
* used to retrieve the type string or settings object for a node
|
||||
* @name get_type(obj [, rules])
|
||||
* @param {mixed} obj the node to find the rules for
|
||||
* @param {Boolean} rules if set to `true` instead of a string the settings object will be returned
|
||||
* @return {String|Object}
|
||||
* @plugin types
|
||||
*/
|
||||
this.get_type = function (obj, rules) {
|
||||
obj = this.get_node(obj);
|
||||
return (!obj) ? false : ( rules ? $.extend({ 'type' : obj.type }, this.settings.types[obj.type]) : obj.type);
|
||||
};
|
||||
/**
|
||||
* used to change a node's type
|
||||
* @name set_type(obj, type)
|
||||
* @param {mixed} obj the node to change
|
||||
* @param {String} type the new type
|
||||
* @plugin types
|
||||
*/
|
||||
this.set_type = function (obj, type) {
|
||||
var m = this._model.data, t, t1, t2, old_type, old_icon, k, d, a;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.set_type(obj[t1], type);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
t = this.settings.types;
|
||||
obj = this.get_node(obj);
|
||||
if(!t[type] || !obj) { return false; }
|
||||
d = this.get_node(obj, true);
|
||||
if (d && d.length) {
|
||||
a = d.children('.jstree-anchor');
|
||||
}
|
||||
old_type = obj.type;
|
||||
old_icon = this.get_icon(obj);
|
||||
obj.type = type;
|
||||
if(old_icon === true || !t[old_type] || (t[old_type].icon !== undefined && old_icon === t[old_type].icon)) {
|
||||
this.set_icon(obj, t[type].icon !== undefined ? t[type].icon : true);
|
||||
}
|
||||
|
||||
// remove old type props
|
||||
if(t[old_type] && t[old_type].li_attr !== undefined && typeof t[old_type].li_attr === 'object') {
|
||||
for (k in t[old_type].li_attr) {
|
||||
if (t[old_type].li_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[obj.id].li_attr['class'] = (m[obj.id].li_attr['class'] || '').replace(t[old_type].li_attr[k], '');
|
||||
if (d) { d.removeClass(t[old_type].li_attr[k]); }
|
||||
}
|
||||
else if (m[obj.id].li_attr[k] === t[old_type].li_attr[k]) {
|
||||
m[obj.id].li_attr[k] = null;
|
||||
if (d) { d.removeAttr(k); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(t[old_type] && t[old_type].a_attr !== undefined && typeof t[old_type].a_attr === 'object') {
|
||||
for (k in t[old_type].a_attr) {
|
||||
if (t[old_type].a_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[obj.id].a_attr['class'] = (m[obj.id].a_attr['class'] || '').replace(t[old_type].a_attr[k], '');
|
||||
if (a) { a.removeClass(t[old_type].a_attr[k]); }
|
||||
}
|
||||
else if (m[obj.id].a_attr[k] === t[old_type].a_attr[k]) {
|
||||
if (k === 'href') {
|
||||
m[obj.id].a_attr[k] = '#';
|
||||
if (a) { a.attr('href', '#'); }
|
||||
}
|
||||
else {
|
||||
delete m[obj.id].a_attr[k];
|
||||
if (a) { a.removeAttr(k); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new props
|
||||
if(t[type].li_attr !== undefined && typeof t[type].li_attr === 'object') {
|
||||
for (k in t[type].li_attr) {
|
||||
if (t[type].li_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (m[obj.id].li_attr[k] === undefined) {
|
||||
m[obj.id].li_attr[k] = t[type].li_attr[k];
|
||||
if (d) {
|
||||
if (k === 'class') {
|
||||
d.addClass(t[type].li_attr[k]);
|
||||
}
|
||||
else {
|
||||
d.attr(k, t[type].li_attr[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[obj.id].li_attr['class'] = t[type].li_attr[k] + ' ' + m[obj.id].li_attr['class'];
|
||||
if (d) { d.addClass(t[type].li_attr[k]); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(t[type].a_attr !== undefined && typeof t[type].a_attr === 'object') {
|
||||
for (k in t[type].a_attr) {
|
||||
if (t[type].a_attr.hasOwnProperty(k)) {
|
||||
if (k === 'id') {
|
||||
continue;
|
||||
}
|
||||
else if (m[obj.id].a_attr[k] === undefined) {
|
||||
m[obj.id].a_attr[k] = t[type].a_attr[k];
|
||||
if (a) {
|
||||
if (k === 'class') {
|
||||
a.addClass(t[type].a_attr[k]);
|
||||
}
|
||||
else {
|
||||
a.attr(k, t[type].a_attr[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (k === 'href' && m[obj.id].a_attr[k] === '#') {
|
||||
m[obj.id].a_attr['href'] = t[type].a_attr['href'];
|
||||
if (a) { a.attr('href', t[type].a_attr['href']); }
|
||||
}
|
||||
else if (k === 'class') {
|
||||
m[obj.id].a_attr['class'] = t[type].a_attr['class'] + ' ' + m[obj.id].a_attr['class'];
|
||||
if (a) { a.addClass(t[type].a_attr[k]); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
// include the types plugin by default
|
||||
// $.jstree.defaults.plugins.push("types");
|
||||
}));
|
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* ### Unique plugin
|
||||
*
|
||||
* Enforces that no nodes with the same name can coexist as siblings.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.unique', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.unique) { return; }
|
||||
|
||||
/**
|
||||
* stores all defaults for the unique plugin
|
||||
* @name $.jstree.defaults.unique
|
||||
* @plugin unique
|
||||
*/
|
||||
$.jstree.defaults.unique = {
|
||||
/**
|
||||
* Indicates if the comparison should be case sensitive. Default is `false`.
|
||||
* @name $.jstree.defaults.unique.case_sensitive
|
||||
* @plugin unique
|
||||
*/
|
||||
case_sensitive : false,
|
||||
/**
|
||||
* Indicates if white space should be trimmed before the comparison. Default is `false`.
|
||||
* @name $.jstree.defaults.unique.trim_whitespace
|
||||
* @plugin unique
|
||||
*/
|
||||
trim_whitespace : false,
|
||||
/**
|
||||
* A callback executed in the instance's scope when a new node is created and the name is already taken, the two arguments are the conflicting name and the counter. The default will produce results like `New node (2)`.
|
||||
* @name $.jstree.defaults.unique.duplicate
|
||||
* @plugin unique
|
||||
*/
|
||||
duplicate : function (name, counter) {
|
||||
return name + ' (' + counter + ')';
|
||||
}
|
||||
};
|
||||
|
||||
$.jstree.plugins.unique = function (options, parent) {
|
||||
this.check = function (chk, obj, par, pos, more) {
|
||||
if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
|
||||
obj = obj && obj.id ? obj : this.get_node(obj);
|
||||
par = par && par.id ? par : this.get_node(par);
|
||||
if(!par || !par.children) { return true; }
|
||||
var n = chk === "rename_node" ? pos : obj.text,
|
||||
c = [],
|
||||
s = this.settings.unique.case_sensitive,
|
||||
w = this.settings.unique.trim_whitespace,
|
||||
m = this._model.data, i, j, t;
|
||||
for(i = 0, j = par.children.length; i < j; i++) {
|
||||
t = m[par.children[i]].text;
|
||||
if (!s) {
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (w) {
|
||||
t = t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
c.push(t);
|
||||
}
|
||||
if(!s) { n = n.toLowerCase(); }
|
||||
if (w) { n = n.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); }
|
||||
switch(chk) {
|
||||
case "delete_node":
|
||||
return true;
|
||||
case "rename_node":
|
||||
t = obj.text || '';
|
||||
if (!s) {
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (w) {
|
||||
t = t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
i = ($.inArray(n, c) === -1 || (obj.text && t === n));
|
||||
if(!i) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_01', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
}
|
||||
return i;
|
||||
case "create_node":
|
||||
i = ($.inArray(n, c) === -1);
|
||||
if(!i) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_04', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
}
|
||||
return i;
|
||||
case "copy_node":
|
||||
i = ($.inArray(n, c) === -1);
|
||||
if(!i) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_02', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
}
|
||||
return i;
|
||||
case "move_node":
|
||||
i = ( (obj.parent === par.id && (!more || !more.is_multi)) || $.inArray(n, c) === -1);
|
||||
if(!i) {
|
||||
this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_03', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
this.create_node = function (par, node, pos, callback, is_loaded) {
|
||||
if(!node || node.text === undefined) {
|
||||
if(par === null) {
|
||||
par = $.jstree.root;
|
||||
}
|
||||
par = this.get_node(par);
|
||||
if(!par) {
|
||||
return parent.create_node.call(this, par, node, pos, callback, is_loaded);
|
||||
}
|
||||
pos = pos === undefined ? "last" : pos;
|
||||
if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
|
||||
return parent.create_node.call(this, par, node, pos, callback, is_loaded);
|
||||
}
|
||||
if(!node) { node = {}; }
|
||||
var tmp, n, dpc, i, j, m = this._model.data, s = this.settings.unique.case_sensitive, w = this.settings.unique.trim_whitespace, cb = this.settings.unique.duplicate, t;
|
||||
n = tmp = this.get_string('New node');
|
||||
dpc = [];
|
||||
for(i = 0, j = par.children.length; i < j; i++) {
|
||||
t = m[par.children[i]].text;
|
||||
if (!s) {
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (w) {
|
||||
t = t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
dpc.push(t);
|
||||
}
|
||||
i = 1;
|
||||
t = n;
|
||||
if (!s) {
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (w) {
|
||||
t = t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
while($.inArray(t, dpc) !== -1) {
|
||||
n = cb.call(this, tmp, (++i)).toString();
|
||||
t = n;
|
||||
if (!s) {
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (w) {
|
||||
t = t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
}
|
||||
node.text = n;
|
||||
}
|
||||
return parent.create_node.call(this, par, node, pos, callback, is_loaded);
|
||||
};
|
||||
};
|
||||
|
||||
// include the unique plugin by default
|
||||
// $.jstree.defaults.plugins.push("unique");
|
||||
}));
|
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* ### Wholerow plugin
|
||||
*
|
||||
* Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers.
|
||||
*/
|
||||
/*globals jQuery, define, exports, require */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.wholerow', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.wholerow) { return; }
|
||||
|
||||
var div = document.createElement('DIV');
|
||||
div.setAttribute('unselectable','on');
|
||||
div.setAttribute('role','presentation');
|
||||
div.className = 'jstree-wholerow';
|
||||
div.innerHTML = ' ';
|
||||
$.jstree.plugins.wholerow = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
|
||||
this.element
|
||||
.on('ready.jstree set_state.jstree', $.proxy(function () {
|
||||
this.hide_dots();
|
||||
}, this))
|
||||
.on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
|
||||
//div.style.height = this._data.core.li_height + 'px';
|
||||
this.get_container_ul().addClass('jstree-wholerow-ul');
|
||||
}, this))
|
||||
.on("deselect_all.jstree", $.proxy(function (e, data) {
|
||||
this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
|
||||
}, this))
|
||||
.on("changed.jstree", $.proxy(function (e, data) {
|
||||
this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
|
||||
var tmp = false, i, j;
|
||||
for(i = 0, j = data.selected.length; i < j; i++) {
|
||||
tmp = this.get_node(data.selected[i], true);
|
||||
if(tmp && tmp.length) {
|
||||
tmp.children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
|
||||
}
|
||||
}
|
||||
}, this))
|
||||
.on("open_node.jstree", $.proxy(function (e, data) {
|
||||
this.get_node(data.node, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
|
||||
}, this))
|
||||
.on("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) {
|
||||
if(e.type === "hover_node" && this.is_disabled(data.node)) { return; }
|
||||
this.get_node(data.node, true).children('.jstree-wholerow')[e.type === "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered');
|
||||
}, this))
|
||||
.on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e) {
|
||||
if (this._data.contextmenu) {
|
||||
e.preventDefault();
|
||||
var tmp = $.Event('contextmenu', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey, pageX : e.pageX, pageY : e.pageY });
|
||||
$(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp);
|
||||
}
|
||||
}, this))
|
||||
/*!
|
||||
.on("mousedown.jstree touchstart.jstree", ".jstree-wholerow", function (e) {
|
||||
if(e.target === e.currentTarget) {
|
||||
var a = $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor");
|
||||
e.target = a[0];
|
||||
a.trigger(e);
|
||||
}
|
||||
})
|
||||
*/
|
||||
.on("click.jstree", ".jstree-wholerow", function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
|
||||
$(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
|
||||
})
|
||||
.on("dblclick.jstree", ".jstree-wholerow", function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
var tmp = $.Event('dblclick', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
|
||||
$(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
|
||||
})
|
||||
.on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
|
||||
$(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
|
||||
}, this))
|
||||
.on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
if(!this.is_disabled(e.currentTarget)) {
|
||||
this.hover_node(e.currentTarget);
|
||||
}
|
||||
return false;
|
||||
}, this))
|
||||
.on("mouseleave.jstree", ".jstree-node", $.proxy(function (e) {
|
||||
this.dehover_node(e.currentTarget);
|
||||
}, this));
|
||||
};
|
||||
this.teardown = function () {
|
||||
if(this.settings.wholerow) {
|
||||
this.element.find(".jstree-wholerow").remove();
|
||||
}
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
this.redraw_node = function(obj, deep, callback, force_render) {
|
||||
obj = parent.redraw_node.apply(this, arguments);
|
||||
if(obj) {
|
||||
var tmp = div.cloneNode(true);
|
||||
//tmp.style.height = this._data.core.li_height + 'px';
|
||||
if($.inArray(obj.id, this._data.core.selected) !== -1) { tmp.className += ' jstree-wholerow-clicked'; }
|
||||
if(this._data.core.focused && this._data.core.focused === obj.id) { tmp.className += ' jstree-wholerow-hovered'; }
|
||||
obj.insertBefore(tmp, obj.childNodes[0]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
// include the wholerow plugin by default
|
||||
// $.jstree.defaults.plugins.push("wholerow");
|
||||
}));
|
657
app/YtManagerApp/static/YtManagerApp/import/jstree/src/misc.js
Normal file
@ -0,0 +1,657 @@
|
||||
/* global jQuery */
|
||||
|
||||
// disable all events
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.trigger = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
// do not forget parent
|
||||
parent.init.call(this, el, options);
|
||||
this._data.trigger.disabled = false;
|
||||
};
|
||||
this.trigger = function (ev, data) {
|
||||
if(!this._data.trigger.disabled) {
|
||||
parent.trigger.call(this, ev, data);
|
||||
}
|
||||
};
|
||||
this.disable_events = function () { this._data.trigger.disabled = true; };
|
||||
this.enable_events = function () { this._data.trigger.disabled = false; };
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// mapping
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
// use this if you need any options
|
||||
$.jstree.defaults.mapper = {
|
||||
option_key : "option_value"
|
||||
};
|
||||
$.jstree.plugins.mapper = function () {
|
||||
this._parse_model_from_json = function (d, p, ps) {
|
||||
// d is the node from the server, it will be called recursively for children,
|
||||
// so you do not need to process at once
|
||||
/* // for example
|
||||
for(var i in d) {
|
||||
if(d.hasOwnProperty(i)) {
|
||||
d[i.toLowerCase()] = d[i];
|
||||
}
|
||||
}
|
||||
*/
|
||||
return parent._parse_model_from_json.call(this, d, p, ps);
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// no hover
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.nohover = function () {
|
||||
this.hover_node = $.noop;
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// force multiple select
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.defaults.multiselect = {};
|
||||
$.jstree.plugins.multiselect = function (options, parent) {
|
||||
this.activate_node = function (obj, e) {
|
||||
e.ctrlKey = true;
|
||||
parent.activate_node.call(this, obj, e);
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// real checkboxes
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
|
||||
var inp = document.createElement("INPUT");
|
||||
inp.type = "checkbox";
|
||||
inp.className = "jstree-checkbox jstree-realcheckbox";
|
||||
|
||||
$.jstree.defaults.realcheckboxes = {};
|
||||
|
||||
$.jstree.plugins.realcheckboxes = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
this._data.realcheckboxes.uto = false;
|
||||
this.element
|
||||
.on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree ready.jstree loaded.jstree', $.proxy(function () {
|
||||
// only if undetermined is in setting
|
||||
if(this._data.realcheckboxes.uto) { clearTimeout(this._data.realcheckboxes.uto); }
|
||||
this._data.realcheckboxes.uto = setTimeout($.proxy(this._realcheckboxes, this), 50);
|
||||
}, this));
|
||||
};
|
||||
this.redraw_node = function(obj, deep, callback, force_draw) {
|
||||
obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
|
||||
if(obj) {
|
||||
var i, j, tmp = null, chk = inp.cloneNode(true);
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
for(i = 0, j = tmp.childNodes.length; i < j; i++) {
|
||||
if(tmp.childNodes[i] && tmp.childNodes[i].className && tmp.childNodes[i].className.indexOf("jstree-checkbox") !== -1) {
|
||||
tmp = tmp.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(tmp && tmp.tagName === "I") {
|
||||
tmp.style.backgroundColor = "transparent";
|
||||
tmp.style.backgroundImage = "none";
|
||||
tmp.appendChild(chk);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
this._realcheckboxes = function () {
|
||||
var ts = this.settings.checkbox.tie_selection;
|
||||
console.log(ts);
|
||||
$('.jstree-realcheckbox').each(function () {
|
||||
this.checked = (!ts && this.parentNode.parentNode.className.indexOf("jstree-checked") !== -1) || (ts && this.parentNode.parentNode.className.indexOf('jstree-clicked') !== -1);
|
||||
this.indeterminate = this.parentNode.className.indexOf("jstree-undetermined") !== -1;
|
||||
this.disabled = this.parentNode.parentNode.className.indexOf("disabled") !== -1;
|
||||
});
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// no state
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.nostate = function () {
|
||||
this.set_state = function (state, callback) {
|
||||
if(callback) { callback.call(this); }
|
||||
this.trigger('set_state');
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// no selected in state
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.noselectedstate = function (options, parent) {
|
||||
this.get_state = function () {
|
||||
var state = parent.get_state.call(this);
|
||||
delete state.core.selected;
|
||||
return state;
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// additional icon on node (outside of anchor)
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
var img = document.createElement('IMG');
|
||||
//img.src = "http://www.dpcd.vic.gov.au/__data/assets/image/0004/30667/help.gif";
|
||||
img.className = "jstree-questionmark";
|
||||
|
||||
$.jstree.defaults.questionmark = $.noop;
|
||||
$.jstree.plugins.questionmark = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
this.element
|
||||
.on("click.jstree", ".jstree-questionmark", $.proxy(function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
this.settings.questionmark.call(this, this.get_node(e.target));
|
||||
}, this));
|
||||
};
|
||||
this.teardown = function () {
|
||||
if(this.settings.questionmark) {
|
||||
this.element.find(".jstree-questionmark").remove();
|
||||
}
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
this.redraw_node = function(obj, deep, callback, force_draw) {
|
||||
obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
|
||||
if(obj) {
|
||||
var tmp = img.cloneNode(true);
|
||||
obj.insertBefore(tmp, obj.childNodes[2]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// auto numbering
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
var span = document.createElement('SPAN');
|
||||
span.className = "jstree-numbering";
|
||||
|
||||
$.jstree.defaults.numbering = {};
|
||||
$.jstree.plugins.numbering = function (options, parent) {
|
||||
this.teardown = function () {
|
||||
if(this.settings.questionmark) {
|
||||
this.element.find(".jstree-numbering").remove();
|
||||
}
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
this.get_number = function (obj) {
|
||||
obj = this.get_node(obj);
|
||||
var ind = $.inArray(obj.id, this.get_node(obj.parent).children) + 1;
|
||||
return obj.parent === '#' ? ind : this.get_number(obj.parent) + '.' + ind;
|
||||
};
|
||||
this.redraw_node = function(obj, deep, callback, force_draw) {
|
||||
var i, j, tmp = null, elm = null, org = this.get_number(obj);
|
||||
obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
|
||||
if(obj) {
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
elm = span.cloneNode(true);
|
||||
elm.innerHTML = org + '. ';
|
||||
tmp.insertBefore(elm, tmp.childNodes[tmp.childNodes.length - 1]);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// additional icon on node (inside anchor)
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
var _s = document.createElement('SPAN');
|
||||
_s.className = 'fa-stack jstree-stackedicon';
|
||||
var _i = document.createElement('I');
|
||||
_i.className = 'jstree-icon';
|
||||
_i.setAttribute('role', 'presentation');
|
||||
|
||||
$.jstree.plugins.stackedicon = function (options, parent) {
|
||||
this.teardown = function () {
|
||||
this.element.find(".jstree-stackedicon").remove();
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
this.redraw_node = function(obj, deep, is_callback, force_render) {
|
||||
obj = parent.redraw_node.apply(this, arguments);
|
||||
if(obj) {
|
||||
var i, j, tmp = null, icon = null, temp = null;
|
||||
for(i = 0, j = obj.childNodes.length; i < j; i++) {
|
||||
if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
|
||||
tmp = obj.childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(tmp) {
|
||||
if(this._model.data[obj.id].state.icons && this._model.data[obj.id].state.icons.length) {
|
||||
icon = _s.cloneNode(false);
|
||||
for(i = 0, j = this._model.data[obj.id].state.icons.length; i < j; i++) {
|
||||
temp = _i.cloneNode(false);
|
||||
temp.className += ' ' + this._model.data[obj.id].state.icons[i];
|
||||
icon.appendChild(temp);
|
||||
}
|
||||
tmp.insertBefore(icon, tmp.childNodes[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// selecting a node opens it
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.selectopens = function (options, parent) {
|
||||
this.bind = function () {
|
||||
parent.bind.call(this);
|
||||
this.element.on('select_node.jstree', function (e, data) { data.instance.open_node(data.node); });
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// object as data
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.defaults.datamodel = {};
|
||||
$.jstree.plugins.datamodel = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
this._data.datamodel = {};
|
||||
parent.init.call(this, el, options);
|
||||
};
|
||||
this._datamodel = function (id, nodes, callback) {
|
||||
var i = 0, j = nodes.length, tmp = [], obj = null;
|
||||
for(; i < j; i++) {
|
||||
this._data.datamodel[nodes[i].getID()] = nodes[i];
|
||||
obj = {
|
||||
id : nodes[i].getID(),
|
||||
text : nodes[i].getText(),
|
||||
children : nodes[i].hasChildren()
|
||||
};
|
||||
if(nodes[i].getExtra) {
|
||||
obj = nodes[i].getExtra(obj); // icon, type
|
||||
}
|
||||
tmp.push(obj);
|
||||
}
|
||||
return this._append_json_data(id, tmp, $.proxy(function (status) {
|
||||
callback.call(this, status);
|
||||
}, this));
|
||||
};
|
||||
this._load_node = function (obj, callback) {
|
||||
var id = obj.id;
|
||||
var nd = obj.id === "#" ? this.settings.core.data : this._data.datamodel[obj.id].getChildren($.proxy(function (nodes) {
|
||||
this._datamodel(id, nodes, callback);
|
||||
}, this));
|
||||
if($.isArray(nd)) {
|
||||
this._datamodel(id, nd, callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
/*
|
||||
demo of the above
|
||||
function treeNode(val) {
|
||||
var id = ++treeNode.counter;
|
||||
this.getID = function () {
|
||||
return id;
|
||||
};
|
||||
this.getText = function () {
|
||||
return val.toString();
|
||||
};
|
||||
this.getExtra = function (obj) {
|
||||
obj.icon = false;
|
||||
return obj;
|
||||
};
|
||||
this.hasChildren = function () {
|
||||
return true;
|
||||
};
|
||||
this.getChildren = function () {
|
||||
return [
|
||||
new treeNode(Math.pow(val, 2)),
|
||||
new treeNode(Math.sqrt(val)),
|
||||
];
|
||||
};
|
||||
}
|
||||
treeNode.counter = 0;
|
||||
|
||||
$('#jstree').jstree({
|
||||
'core': {
|
||||
'data': [
|
||||
new treeNode(2),
|
||||
new treeNode(3),
|
||||
new treeNode(4),
|
||||
new treeNode(5)
|
||||
]
|
||||
},
|
||||
plugins : ['datamodel']
|
||||
});
|
||||
*/
|
||||
|
||||
// untested sample plugin to keep all nodes in the DOM
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
$.jstree.plugins.dom = function (options, parent) {
|
||||
this.redraw_node = function (node, deep, is_callback, force_render) {
|
||||
return parent.redraw_node.call(this, node, deep, is_callback, true);
|
||||
};
|
||||
this.close_node = function (obj, animation) {
|
||||
var t1, t2, t, d;
|
||||
if($.isArray(obj)) {
|
||||
obj = obj.slice();
|
||||
for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
|
||||
this.close_node(obj[t1], animation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
obj = this.get_node(obj);
|
||||
if(!obj || obj.id === $.jstree.root) {
|
||||
return false;
|
||||
}
|
||||
if(this.is_closed(obj)) {
|
||||
return false;
|
||||
}
|
||||
animation = animation === undefined ? this.settings.core.animation : animation;
|
||||
t = this;
|
||||
d = this.get_node(obj, true);
|
||||
if(d.length) {
|
||||
if(!animation) {
|
||||
d[0].className = d[0].className.replace('jstree-open', 'jstree-closed');
|
||||
d.attr("aria-expanded", false);
|
||||
}
|
||||
else {
|
||||
d
|
||||
.children(".jstree-children").attr("style","display:block !important").end()
|
||||
.removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false)
|
||||
.children(".jstree-children").stop(true, true).slideUp(animation, function () {
|
||||
this.style.display = "";
|
||||
t.trigger("after_close", { "node" : obj });
|
||||
});
|
||||
}
|
||||
}
|
||||
obj.state.opened = false;
|
||||
this.trigger('close_node',{ "node" : obj });
|
||||
if(!animation || !d.length) {
|
||||
this.trigger("after_close", { "node" : obj });
|
||||
}
|
||||
};
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
// customize plugin by @Lusito
|
||||
// https://github.com/Lusito/jstree/blob/node-customize/src/jstree-node-customize.js
|
||||
/**
|
||||
* ### Node Customize plugin
|
||||
*
|
||||
* Allows to customize nodes when they are drawn.
|
||||
*/
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.node_customize', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.node_customize) { return; }
|
||||
|
||||
/**
|
||||
* the settings object.
|
||||
* key is the attribute name to select the customizer function from switch.
|
||||
* switch is a key => function(el, node) map.
|
||||
* default: function(el, node) will be called if the type could not be mapped
|
||||
* @name $.jstree.defaults.node_customize
|
||||
* @plugin node_customize
|
||||
*/
|
||||
$.jstree.defaults.node_customize = {
|
||||
"key": "type",
|
||||
"switch": {},
|
||||
"default": null
|
||||
};
|
||||
|
||||
$.jstree.plugins.node_customize = function (options, parent) {
|
||||
this.redraw_node = function (obj, deep, callback, force_draw) {
|
||||
var node_id = obj;
|
||||
var el = parent.redraw_node.apply(this, arguments);
|
||||
if (el) {
|
||||
var node = this._model.data[node_id];
|
||||
var cfg = this.settings.node_customize;
|
||||
var key = cfg.key;
|
||||
var type = (node && node.original && node.original[key]);
|
||||
var customizer = (type && cfg.switch[type]) || cfg.default;
|
||||
if(customizer)
|
||||
customizer(el, node);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// parentsload plugin by @ashl1
|
||||
/**
|
||||
* ### Parentsload plugin
|
||||
*
|
||||
* Change load_node() functionality in jsTree, to possible load not yes downloaded node with all it parent in a single request (only useful with lazy loading).
|
||||
*
|
||||
* version 1.0.0 (Alexey Shildyakov - ashl1future@gmail.com)
|
||||
* 2015: Compatible with jsTree-3.2.1
|
||||
*/
|
||||
/*globals jQuery, define, exports, require, document */
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.parentsload', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.parentsload) { return; }
|
||||
|
||||
/**
|
||||
* parentsload configuration
|
||||
*
|
||||
* The configuration syntax is almost the same as for core.data option. You must set parenstload.data the following:
|
||||
*
|
||||
* parentsload: {
|
||||
* data: function(){} // this function overwrites core data.data options
|
||||
* }
|
||||
*
|
||||
* OR
|
||||
*
|
||||
* parentsload: {
|
||||
* data: {
|
||||
* url: function(node){} OR string,
|
||||
* data: function(node){} OR associative array as json{data} jQuery parameter
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* In last case at least on of 'url' or 'data' must be presented.
|
||||
*
|
||||
* At first, the plugin load_node() detects if the node already downloaded. If is - uses the core.data settings, if not - uses parentsload.data settings
|
||||
* to fetch in one query the specified node and all its parent. The data must be in the first mentioned JSON format with set nested children[].
|
||||
* Each node level should consist of all nodes on the level to properly work with the tree in the future. Otherwise, you must manually call load_node
|
||||
* on every parent node to fetch all children nodes on that level.
|
||||
*
|
||||
* @name $.jstree.defaults.parentsload
|
||||
* @plugin parentsload
|
||||
*/
|
||||
$.jstree.defaults.parentsload = null;
|
||||
$.jstree.plugins.parentsload = function (options, parent) {
|
||||
this.init = function (el, options) {
|
||||
parent.init.call(this, el, options);
|
||||
this.patch_data()
|
||||
};
|
||||
this.patch_data = function(){
|
||||
var parentsloadSettings = this.settings.parentsload;
|
||||
var jsTreeDataSettings = this.settings.core.data;
|
||||
var self = this;
|
||||
|
||||
var callError = function(number, message) {
|
||||
self._data.core.last_error = { 'error' : 'configuration', 'plugin' : 'parentsload', 'id' : 'parentsload_' + number, 'reason' : message, 'data' : JSON.stringify({config: parentsloadSettings}) };
|
||||
self.settings.core.error.call(self, self._data.core.last_error);
|
||||
}
|
||||
|
||||
if(!parentsloadSettings) {
|
||||
callError('01', 'The configuration must be presented')
|
||||
return
|
||||
}
|
||||
parentsloadSettings = parentsloadSettings.data;
|
||||
|
||||
var patchSettingsProperty = function (propertyName) {
|
||||
var property = parentsloadSettings[propertyName],
|
||||
coreProperty = jsTreeDataSettings[propertyName];
|
||||
if (property) {
|
||||
jsTreeDataSettings[propertyName] = function(node) {
|
||||
if (this.get_node(node).parentsload_required) {
|
||||
if ($.isFunction(property)) {
|
||||
return property.call(this, node)
|
||||
} else {// (typeof property === 'string')
|
||||
return property
|
||||
}
|
||||
} else {
|
||||
if ($.isFunction(coreProperty)) {
|
||||
return coreProperty.call(this, node)
|
||||
} else { // (typeof coreProperty === 'string')
|
||||
return coreProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* else {
|
||||
use jstree the same data[propertyName] settings
|
||||
}*/
|
||||
}
|
||||
|
||||
if($.isFunction(parentsloadSettings)) {
|
||||
this.settings.data = parentsloadSettings
|
||||
} else if (typeof parentsloadSettings === 'object') {
|
||||
if (! (parentsloadSettings.url || parentsloadSettings.data)) {
|
||||
callError('02', 'The "data.url" or "data.data" must be presented in configuration')
|
||||
return
|
||||
}
|
||||
patchSettingsProperty('url')
|
||||
patchSettingsProperty('data')
|
||||
|
||||
} else {
|
||||
callError('03', 'The appropriate "data.url" or "data.data" must be presented in configuration')
|
||||
}
|
||||
}
|
||||
|
||||
this.load_node = function (obj, callback) {
|
||||
if($.isArray(obj)) {
|
||||
// FIXME: _load_nodes will not load nodes not presented in the tree
|
||||
this._load_nodes(obj.slice(), callback);
|
||||
return true;
|
||||
}
|
||||
var foundObj = this.get_node(obj);
|
||||
if (foundObj) {
|
||||
return parent.load_node.apply(this, arguments)
|
||||
} else {
|
||||
// node hasn't been loaded
|
||||
var id = obj.id? obj.id: obj;
|
||||
this._model.data[id] = {
|
||||
id : id,
|
||||
parent : '#',
|
||||
parents : [],
|
||||
children : [],
|
||||
children_d : [],
|
||||
state : { loaded : false },
|
||||
li_attr : {},
|
||||
a_attr : {},
|
||||
parentsload_required : true,
|
||||
};
|
||||
return parent.load_node.call(this, obj, function(obj, status){
|
||||
obj.parentsload_required = !status
|
||||
callback.call(this, obj, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
// conditional deselect
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.conditionaldeselect', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.conditionaldeselect) { return; }
|
||||
$.jstree.defaults.conditionaldeselect = function () { return true; };
|
||||
$.jstree.plugins.conditionaldeselect = function (options, parent) {
|
||||
// own function
|
||||
this.deselect_node = function (obj, supress_event, e) {
|
||||
if(this.settings.conditionaldeselect.call(this, this.get_node(obj), e)) {
|
||||
return parent.deselect_node.call(this, obj, supress_event, e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}));
|
||||
|
||||
// conditional close
|
||||
(function (factory) {
|
||||
"use strict";
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('jstree.conditionalclose', ['jquery','jstree'], factory);
|
||||
}
|
||||
else if(typeof exports === 'object') {
|
||||
factory(require('jquery'), require('jstree'));
|
||||
}
|
||||
else {
|
||||
factory(jQuery, jQuery.jstree);
|
||||
}
|
||||
}(function ($, jstree, undefined) {
|
||||
"use strict";
|
||||
|
||||
if($.jstree.plugins.conditionalclose) { return; }
|
||||
$.jstree.defaults.conditionalclose = function () { return true; };
|
||||
$.jstree.plugins.conditionalclose = function (options, parent) {
|
||||
// own function
|
||||
this.close_node = function (obj, animation) {
|
||||
if(this.settings.conditionalclose.close.call(this, this.get_node(obj), e)) {
|
||||
return parent.deselect_node.call(this, obj, animation);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}));
|
@ -0,0 +1 @@
|
||||
}));
|
@ -0,0 +1,93 @@
|
||||
/*global jQuery */
|
||||
// wrap in IIFE and pass jQuery as $
|
||||
(function ($, undefined) {
|
||||
"use strict";
|
||||
|
||||
// some private plugin stuff if needed
|
||||
var private_var = null;
|
||||
|
||||
// extending the defaults
|
||||
$.jstree.defaults.sample = {
|
||||
sample_option : 'sample_val'
|
||||
};
|
||||
|
||||
// the actual plugin code
|
||||
$.jstree.plugins.sample = function (options, parent) {
|
||||
// own function
|
||||
this.sample_function = function (arg) {
|
||||
// you can chain this method if needed and available
|
||||
if(parent.sample_function) { parent.sample_function.call(this, arg); }
|
||||
};
|
||||
|
||||
// *SPECIAL* FUNCTIONS
|
||||
this.init = function (el, options) {
|
||||
// do not forget parent
|
||||
parent.init.call(this, el, options);
|
||||
};
|
||||
// bind events if needed
|
||||
this.bind = function () {
|
||||
// call parent function first
|
||||
parent.bind.call(this);
|
||||
// do(stuff);
|
||||
};
|
||||
// unbind events if needed (all in jquery namespace are taken care of by the core)
|
||||
this.unbind = function () {
|
||||
// do(stuff);
|
||||
// call parent function last
|
||||
parent.unbind.call(this);
|
||||
};
|
||||
this.teardown = function () {
|
||||
// do not forget parent
|
||||
parent.teardown.call(this);
|
||||
};
|
||||
// state management - get and restore
|
||||
this.get_state = function () {
|
||||
// always get state from parent first
|
||||
var state = parent.get_state.call(this);
|
||||
// add own stuff to state
|
||||
state.sample = { 'var' : 'val' };
|
||||
return state;
|
||||
};
|
||||
this.set_state = function (state, callback) {
|
||||
// only process your part if parent returns true
|
||||
// there will be multiple times with false
|
||||
if(parent.set_state.call(this, state, callback)) {
|
||||
// check the key you set above
|
||||
if(state.sample) {
|
||||
// do(stuff); // like calling this.sample_function(state.sample.var);
|
||||
// remove your part of the state, call again and RETURN FALSE, the next cycle will be TRUE
|
||||
delete state.sample;
|
||||
this.set_state(state, callback);
|
||||
return false;
|
||||
}
|
||||
// return true if your state is gone (cleared in the previous step)
|
||||
return true;
|
||||
}
|
||||
// parent was false - return false too
|
||||
return false;
|
||||
};
|
||||
// node transportation
|
||||
this.get_json = function (obj, options, flat) {
|
||||
// get the node from the parent
|
||||
var tmp = parent.get_json.call(this, obj, options, flat), i, j;
|
||||
if($.isArray(tmp)) {
|
||||
for(i = 0, j = tmp.length; i < j; i++) {
|
||||
tmp[i].sample = 'value';
|
||||
}
|
||||
}
|
||||
else {
|
||||
tmp.sample = 'value';
|
||||
}
|
||||
// return the original / modified node
|
||||
return tmp;
|
||||
};
|
||||
};
|
||||
|
||||
// attach to document ready if needed
|
||||
$(function () {
|
||||
// do(stuff);
|
||||
});
|
||||
|
||||
// you can include the sample plugin in all instances by default
|
||||
$.jstree.defaults.plugins.push("sample");
|
||||
})(jQuery);
|
@ -0,0 +1,93 @@
|
||||
// base jstree
|
||||
.jstree-node, .jstree-children, .jstree-container-ul { display:block; margin:0; padding:0; list-style-type:none; list-style-image:none; }
|
||||
.jstree-node { white-space:nowrap; }
|
||||
.jstree-anchor { display:inline-block; color:black; white-space:nowrap; padding:0 4px 0 1px; margin:0; vertical-align:top; }
|
||||
.jstree-anchor:focus { outline:0; }
|
||||
.jstree-anchor, .jstree-anchor:link, .jstree-anchor:visited, .jstree-anchor:hover, .jstree-anchor:active { text-decoration:none; color:inherit; }
|
||||
.jstree-icon { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; }
|
||||
.jstree-icon:empty { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; }
|
||||
.jstree-ocl { cursor:pointer; }
|
||||
.jstree-leaf > .jstree-ocl { cursor:default; }
|
||||
.jstree .jstree-open > .jstree-children { display:block; }
|
||||
.jstree .jstree-closed > .jstree-children,
|
||||
.jstree .jstree-leaf > .jstree-children { display:none; }
|
||||
.jstree-anchor > .jstree-themeicon { margin-right:2px; }
|
||||
.jstree-no-icons .jstree-themeicon,
|
||||
.jstree-anchor > .jstree-themeicon-hidden { display:none; }
|
||||
.jstree-hidden, .jstree-node.jstree-hidden { display:none; }
|
||||
|
||||
// base jstree rtl
|
||||
.jstree-rtl {
|
||||
.jstree-anchor { padding:0 1px 0 4px; }
|
||||
.jstree-anchor > .jstree-themeicon { margin-left:2px; margin-right:0; }
|
||||
.jstree-node { margin-left:0; }
|
||||
.jstree-container-ul > .jstree-node { margin-right:0; }
|
||||
}
|
||||
|
||||
// base jstree wholerow
|
||||
.jstree-wholerow-ul {
|
||||
position:relative;
|
||||
display:inline-block;
|
||||
min-width:100%;
|
||||
.jstree-leaf > .jstree-ocl { cursor:pointer; }
|
||||
.jstree-anchor, .jstree-icon { position:relative; }
|
||||
.jstree-wholerow { width:100%; cursor:pointer; position:absolute; left:0; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; }
|
||||
}
|
||||
|
||||
// base contextmenu
|
||||
.jstree-contextmenu .jstree-anchor {
|
||||
-webkit-user-select: none; /* disable selection/Copy of UIWebView */
|
||||
-webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */
|
||||
}
|
||||
.vakata-context {
|
||||
display:none;
|
||||
&, ul { margin:0; padding:2px; position:absolute; background:#f5f5f5; border:1px solid #979797; box-shadow:2px 2px 2px #999999; }
|
||||
ul { list-style:none; left:100%; margin-top:-2.7em; margin-left:-4px; }
|
||||
.vakata-context-right ul { left:auto; right:100%; margin-left:auto; margin-right:-4px; }
|
||||
li {
|
||||
list-style:none;
|
||||
> a {
|
||||
display:block; padding:0 2em 0 2em; text-decoration:none; width:auto; color:black; white-space:nowrap; line-height:2.4em; text-shadow:1px 1px 0 white; border-radius:1px;
|
||||
&:hover { position:relative; background-color:#e8eff7; box-shadow:0 0 2px #0a6aa1; }
|
||||
&.vakata-context-parent { background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAIORI4JlrqN1oMSnmmZDQUAOw=="); background-position:right center; background-repeat:no-repeat; }
|
||||
}
|
||||
> a:focus { outline:0; }
|
||||
}
|
||||
.vakata-context-hover > a { position:relative; background-color:#e8eff7; box-shadow:0 0 2px #0a6aa1; }
|
||||
.vakata-context-separator {
|
||||
> a, > a:hover { background:white; border:0; border-top:1px solid #e2e3e3; height:1px; min-height:1px; max-height:1px; padding:0; margin:0 0 0 2.4em; border-left:1px solid #e0e0e0; text-shadow:0 0 0 transparent; box-shadow:0 0 0 transparent; border-radius:0; }
|
||||
}
|
||||
.vakata-contextmenu-disabled {
|
||||
a, a:hover { color:silver; background-color:transparent; border:0; box-shadow:0 0 0; }
|
||||
> a > i { filter: grayscale(100%); }
|
||||
}
|
||||
li > a {
|
||||
> i { text-decoration:none; display:inline-block; width:2.4em; height:2.4em; background:transparent; margin:0 0 0 -2em; vertical-align:top; text-align:center; line-height:2.4em; }
|
||||
> i:empty { width:2.4em; line-height:2.4em; }
|
||||
.vakata-contextmenu-sep { display:inline-block; width:1px; height:2.4em; background:white; margin:0 0.5em 0 0; border-left:1px solid #e2e3e3; }
|
||||
}
|
||||
.vakata-contextmenu-shortcut { font-size:0.8em; color:silver; opacity:0.5; display:none; }
|
||||
}
|
||||
.vakata-context-rtl {
|
||||
ul { left:auto; right:100%; margin-left:auto; margin-right:-4px; }
|
||||
li > a.vakata-context-parent { background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAINjI+AC7rWHIsPtmoxLAA7"); background-position:left center; background-repeat:no-repeat; }
|
||||
.vakata-context-separator > a { margin:0 2.4em 0 0; border-left:0; border-right:1px solid #e2e3e3;}
|
||||
.vakata-context-left ul { right:auto; left:100%; margin-left:-4px; margin-right:auto; }
|
||||
li > a {
|
||||
> i { margin:0 -2em 0 0; }
|
||||
.vakata-contextmenu-sep { margin:0 0 0 0.5em; border-left-color:white; background:#e2e3e3; }
|
||||
}
|
||||
}
|
||||
|
||||
// base drag'n'drop
|
||||
#jstree-marker { position: absolute; top:0; left:0; margin:-5px 0 0 0; padding:0; border-right:0; border-top:5px solid transparent; border-bottom:5px solid transparent; border-left:5px solid; width:0; height:0; font-size:0; line-height:0; }
|
||||
#jstree-dnd {
|
||||
line-height:16px;
|
||||
margin:0;
|
||||
padding:4px;
|
||||
.jstree-icon,
|
||||
.jstree-copy { display:inline-block; text-decoration:none; margin:0 2px 0 0; padding:0; width:16px; height:16px; }
|
||||
.jstree-ok { background:green; }
|
||||
.jstree-er { background:red; }
|
||||
.jstree-copy { margin:0 2px 0 2px; }
|
||||
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,50 @@
|
||||
/* jsTree default dark theme */
|
||||
@theme-name: default-dark;
|
||||
@hovered-bg-color: #555;
|
||||
@hovered-shadow-color: #555;
|
||||
@disabled-color: #666666;
|
||||
@disabled-bg-color: #333333;
|
||||
@clicked-bg-color: #5fa2db;
|
||||
@clicked-shadow-color: #666666;
|
||||
@clicked-gradient-color-1: #5fa2db;
|
||||
@clicked-gradient-color-2: #5fa2db;
|
||||
@search-result-color: #ffffff;
|
||||
@mobile-wholerow-bg-color: #333333;
|
||||
@mobile-wholerow-shadow: #111111;
|
||||
@mobile-wholerow-bordert: #666;
|
||||
@mobile-wholerow-borderb: #000;
|
||||
@responsive: true;
|
||||
@image-path: "";
|
||||
@base-height: 40px;
|
||||
|
||||
@import "../mixins.less";
|
||||
@import "../base.less";
|
||||
@import "../main.less";
|
||||
|
||||
.jstree-@{theme-name} {
|
||||
background:#333;
|
||||
.jstree-anchor { color:#999; text-shadow:1px 1px 0 rgba(0,0,0,0.5); }
|
||||
.jstree-clicked, .jstree-checked { color:white; }
|
||||
.jstree-hovered { color:white; }
|
||||
#jstree-marker& {
|
||||
border-left-color:#999;
|
||||
background:transparent;
|
||||
}
|
||||
.jstree-anchor > .jstree-icon { opacity:0.75; }
|
||||
.jstree-clicked > .jstree-icon,
|
||||
.jstree-hovered > .jstree-icon,
|
||||
.jstree-checked > .jstree-icon { opacity:1; }
|
||||
}
|
||||
// theme variants
|
||||
.jstree-@{theme-name} {
|
||||
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); }
|
||||
&.jstree-rtl .jstree-last { background:transparent; }
|
||||
}
|
||||
.jstree-@{theme-name}-small {
|
||||
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); }
|
||||
&.jstree-rtl .jstree-last { background:transparent; }
|
||||
}
|
||||
.jstree-@{theme-name}-large {
|
||||
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); }
|
||||
&.jstree-rtl .jstree-last { background:transparent; }
|
||||
}
|
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 5.9 KiB |