Integrate the Cabinets app into the core.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-03-09 01:58:58 -04:00
parent 286e9517c3
commit ff703b32a2
98 changed files with 30956 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ the user links
- Tags are alphabetically ordered by label (GitLab #342).
- Stop loading theme fonts from the web (GitLab #343).
- Add support for attaching multiple tags (GitLab #307).
- Integrate the Cabinets app.
2.1.10 (2017-02-13)
===================

View File

@@ -61,6 +61,7 @@ resolved to '/api/documents/<pk>/pages/<page_pk>/pages'.
Other changes
-------------
- The Cabinets app integration.
- Add "Check now" button to interval sources.
- Remove the installation app
- Add support for page search

View File

@@ -0,0 +1,3 @@
from __future__ import unicode_literals
default_app_config = 'cabinets.apps.CabinetsApp'

View File

@@ -0,0 +1,13 @@
from __future__ import unicode_literals
from django.contrib import admin
from .models import Cabinet
from mptt.admin import MPTTModelAdmin
@admin.register(Cabinet)
class CabinetAdmin(MPTTModelAdmin):
filter_horizontal = ('documents',)
list_display = ('label',)

View File

@@ -0,0 +1,220 @@
from __future__ import absolute_import, unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view
from rest_api.filters import MayanObjectPermissionsFilter
from rest_api.permissions import MayanPermission
from .models import Cabinet
from .permissions import (
permission_cabinet_add_document, permission_cabinet_create,
permission_cabinet_delete, permission_cabinet_edit,
permission_cabinet_remove_document, permission_cabinet_view
)
from .serializers import (
CabinetDocumentSerializer, CabinetSerializer, NewCabinetDocumentSerializer,
WritableCabinetSerializer
)
class APIDocumentCabinetListView(generics.ListAPIView):
"""
Returns a list of all the cabinets to which a document belongs.
"""
serializer_class = CabinetSerializer
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_cabinet_view,)}
def get_queryset(self):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=document
)
queryset = document.document_cabinets().all()
return queryset
class APICabinetListView(generics.ListCreateAPIView):
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_cabinet_view,)}
mayan_view_permissions = {'POST': (permission_cabinet_create,)}
permission_classes = (MayanPermission,)
queryset = Cabinet.objects.all()
def get_serializer_class(self):
if self.request.method == 'GET':
return CabinetSerializer
elif self.request.method == 'POST':
return WritableCabinetSerializer
def get(self, *args, **kwargs):
"""
Returns a list of all the cabinets.
"""
return super(APICabinetListView, self).get(*args, **kwargs)
def post(self, *args, **kwargs):
"""
Create a new cabinet.
"""
return super(APICabinetListView, self).post(*args, **kwargs)
class APICabinetView(generics.RetrieveUpdateDestroyAPIView):
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {
'GET': (permission_cabinet_view,),
'PUT': (permission_cabinet_edit,),
'PATCH': (permission_cabinet_edit,),
'DELETE': (permission_cabinet_delete,)
}
permission_classes = (MayanPermission,)
queryset = Cabinet.objects.all()
def delete(self, *args, **kwargs):
"""
Delete the selected cabinet.
"""
return super(APICabinetView, self).delete(*args, **kwargs)
def get(self, *args, **kwargs):
"""
Returns the details of the selected cabinet.
"""
return super(APICabinetView, self).get(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return CabinetSerializer
else:
return WritableCabinetSerializer
def patch(self, *args, **kwargs):
"""
Edit the selected cabinet.
"""
return super(APICabinetView, self).patch(*args, **kwargs)
def put(self, *args, **kwargs):
"""
Edit the selected cabinet.
"""
return super(APICabinetView, self).put(*args, **kwargs)
class APICabinetDocumentListView(generics.ListCreateAPIView):
"""
Returns a list of all the documents contained in a particular cabinet.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {
'GET': (permission_cabinet_view,),
'POST': (permission_cabinet_add_document,)
}
def get_serializer_class(self):
if self.request.method == 'GET':
return CabinetDocumentSerializer
elif self.request.method == 'POST':
return NewCabinetDocumentSerializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'cabinet': self.get_cabinet(),
'format': self.format_kwarg,
'request': self.request,
'view': self
}
def get_cabinet(self):
return get_object_or_404(Cabinet, pk=self.kwargs['pk'])
def get_queryset(self):
cabinet = self.get_cabinet()
return AccessControlList.objects.filter_by_access(
permission_document_view, self.request.user,
queryset=cabinet.documents.all()
)
def perform_create(self, serializer):
serializer.save(cabinet=self.get_cabinet())
def post(self, request, *args, **kwargs):
"""
Add a document to the selected cabinet.
"""
return super(APICabinetDocumentListView, self).post(
request, *args, **kwargs
)
class APICabinetDocumentView(generics.RetrieveDestroyAPIView):
filter_backends = (MayanObjectPermissionsFilter,)
lookup_url_kwarg = 'document_pk'
mayan_object_permissions = {
'GET': (permission_cabinet_view,),
'DELETE': (permission_cabinet_remove_document,)
}
serializer_class = CabinetDocumentSerializer
def delete(self, request, *args, **kwargs):
"""
Remove a document from the selected cabinet.
"""
return super(APICabinetDocumentView, self).delete(
request, *args, **kwargs
)
def get(self, *args, **kwargs):
"""
Returns the details of the selected cabinet document.
"""
return super(APICabinetDocumentView, self).get(*args, **kwargs)
def get_cabinet(self):
return get_object_or_404(Cabinet, pk=self.kwargs['pk'])
def get_queryset(self):
return self.get_cabinet().documents.all()
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'cabinet': self.get_cabinet(),
'format': self.format_kwarg,
'request': self.request,
'view': self
}
def perform_destroy(self, instance):
self.get_cabinet().documents.remove(instance)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=instance
)
serializer = self.get_serializer(instance)
return Response(serializer.data)

106
mayan/apps/cabinets/apps.py Normal file
View File

@@ -0,0 +1,106 @@
from __future__ import unicode_literals
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission
from acls.permissions import permission_acl_edit, permission_acl_view
from common import (
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object,
menu_sidebar, menu_secondary
)
from rest_api.classes import APIEndPoint
from .links import (
link_cabinet_list, link_document_cabinet_list,
link_document_cabinet_remove, link_cabinet_add_document,
link_cabinet_add_multiple_documents, link_cabinet_child_add,
link_cabinet_create, link_cabinet_delete, link_cabinet_edit,
link_cabinet_view, link_custom_acl_list,
link_multiple_document_cabinet_remove
)
from .menus import menu_cabinets
from .permissions import (
permission_cabinet_add_document, permission_cabinet_delete,
permission_cabinet_edit, permission_cabinet_remove_document,
permission_cabinet_view
)
class CabinetsApp(MayanAppConfig):
name = 'cabinets'
test = True
verbose_name = _('Cabinets')
def ready(self):
super(CabinetsApp, self).ready()
Document = apps.get_model(
app_label='documents', model_name='Document'
)
DocumentCabinet = self.get_model('DocumentCabinet')
Cabinet = self.get_model('Cabinet')
APIEndPoint(app=self, version_string='1')
Document.add_to_class(
'document_cabinets',
lambda document: DocumentCabinet.objects.filter(documents=document)
)
ModelPermission.register(
model=Document, permissions=(
permission_cabinet_add_document,
permission_cabinet_remove_document
)
)
ModelPermission.register(
model=Cabinet, permissions=(
permission_acl_edit, permission_acl_view,
permission_cabinet_delete, permission_cabinet_edit,
permission_cabinet_view
)
)
menu_facet.bind_links(
links=(link_document_cabinet_list,), sources=(Document,)
)
menu_cabinets.bind_links(
links=(
link_cabinet_list, link_cabinet_create
)
)
menu_main.bind_links(links=(menu_cabinets,), position=98)
menu_multi_item.bind_links(
links=(
link_cabinet_add_multiple_documents,
link_multiple_document_cabinet_remove
), sources=(Document,)
)
menu_object.bind_links(
links=(
link_cabinet_view,
), sources=(DocumentCabinet, )
)
menu_object.bind_links(
links=(
link_cabinet_view, link_cabinet_edit,
link_custom_acl_list, link_cabinet_delete
), sources=(Cabinet,)
)
menu_sidebar.bind_links(
links=(link_cabinet_child_add,), sources=(Cabinet,)
)
menu_sidebar.bind_links(
links=(link_cabinet_add_document, link_document_cabinet_remove),
sources=(
'cabinets:document_cabinet_list',
'cabinets:cabinet_add_document',
'cabinets:document_cabinet_remove'
)
)

View File

@@ -0,0 +1,33 @@
from __future__ import absolute_import, unicode_literals
import logging
from django import forms
from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from .models import Cabinet
logger = logging.getLogger(__name__)
class CabinetListForm(forms.Form):
def __init__(self, *args, **kwargs):
help_text = kwargs.pop('help_text', None)
permission = kwargs.pop('permission', None)
queryset = kwargs.pop('queryset', Cabinet.objects.all())
user = kwargs.pop('user', None)
logger.debug('user: %s', user)
super(CabinetListForm, self).__init__(*args, **kwargs)
queryset = AccessControlList.objects.filter_by_access(
permission=permission, user=user, queryset=queryset
)
self.fields['cabinets'] = forms.ModelMultipleChoiceField(
label=_('Cabinets'), help_text=help_text,
queryset=queryset, required=False,
widget=forms.SelectMultiple(attrs={'class': 'select2'})
)

View File

@@ -0,0 +1,76 @@
from __future__ import absolute_import, unicode_literals
import copy
from django.utils.translation import ugettext_lazy as _
from acls.links import link_acl_list
from documents.permissions import permission_document_view
from navigation import Link
from .permissions import (
permission_cabinet_add_document, permission_cabinet_create,
permission_cabinet_delete, permission_cabinet_edit,
permission_cabinet_view, permission_cabinet_remove_document
)
# Document links
link_document_cabinet_list = Link(
icon='fa fa-columns', permissions=(permission_document_view,),
text=_('Cabinets'), view='cabinets:document_cabinet_list',
args='resolved_object.pk'
)
link_document_cabinet_remove = Link(
args='resolved_object.pk',
permissions=(permission_cabinet_remove_document,),
text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove'
)
link_cabinet_add_document = Link(
permissions=(permission_cabinet_add_document,),
text=_('Add to a cabinets'), view='cabinets:cabinet_add_document',
args='object.pk'
)
link_cabinet_add_multiple_documents = Link(
text=_('Add to cabinets'), view='cabinets:cabinet_add_multiple_documents'
)
link_multiple_document_cabinet_remove = Link(
text=_('Remove from cabinets'),
view='cabinets:multiple_document_cabinet_remove'
)
# Cabinet links
def cabinet_is_root(context):
return context[
'resolved_object'
].is_root_node()
link_custom_acl_list = copy.copy(link_acl_list)
link_custom_acl_list.condition = cabinet_is_root
link_cabinet_child_add = Link(
permissions=(permission_cabinet_create,), text=_('Add new level'),
view='cabinets:cabinet_child_add', args='object.pk'
)
link_cabinet_create = Link(
icon='fa fa-plus', permissions=(permission_cabinet_create,),
text=_('Create cabinet'), view='cabinets:cabinet_create'
)
link_cabinet_delete = Link(
permissions=(permission_cabinet_delete,), tags='dangerous',
text=_('Delete'), view='cabinets:cabinet_delete', args='object.pk'
)
link_cabinet_edit = Link(
permissions=(permission_cabinet_edit,), text=_('Edit'),
view='cabinets:cabinet_edit', args='object.pk'
)
link_cabinet_list = Link(
icon='fa fa-columns', text=_('All'), view='cabinets:cabinet_list'
)
link_cabinet_view = Link(
permissions=(permission_cabinet_view,), text=_('Details'),
view='cabinets:cabinet_view', args='object.pk'
)

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from navigation import Menu
menu_cabinets = Menu(
icon='fa fa-columns', label=_('Cabinets'), name='cabinets menu'
)

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-24 07:37
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('documents', '0034_auto_20160509_2321'),
]
operations = [
migrations.CreateModel(
name='Cabinet',
fields=[
(
'id', models.AutoField(
auto_created=True, primary_key=True, serialize=False,
verbose_name='ID'
)
),
(
'label', models.CharField(
max_length=128, verbose_name='Label'
)
),
(
'lft', models.PositiveIntegerField(
db_index=True, editable=False
)
),
(
'rght', models.PositiveIntegerField(
db_index=True, editable=False
)
),
(
'tree_id', models.PositiveIntegerField(
db_index=True, editable=False
)
),
(
'level', models.PositiveIntegerField(
db_index=True, editable=False
)
),
(
'documents', models.ManyToManyField(
blank=True, related_name='cabinets',
to='documents.Document', verbose_name='Documents'
)
),
(
'parent', mptt.fields.TreeForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='children', to='cabinets.Cabinet'
)
),
],
options={
'ordering': ('parent__label', 'label'),
'verbose_name': 'Cabinet',
'verbose_name_plural': 'Cabinets',
},
),
migrations.CreateModel(
name='DocumentCabinet',
fields=[
],
options={
'verbose_name': 'Document cabinet',
'proxy': True,
'verbose_name_plural': 'Document cabinets',
},
bases=('cabinets.cabinet',),
),
migrations.AlterUniqueTogether(
name='cabinet',
unique_together=set([('parent', 'label')]),
),
]

View File

@@ -0,0 +1,87 @@
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
from acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view
@python_2_unicode_compatible
class Cabinet(MPTTModel):
parent = TreeForeignKey(
'self', blank=True, db_index=True, null=True, related_name='children'
)
label = models.CharField(max_length=128, verbose_name=_('Label'))
documents = models.ManyToManyField(
Document, blank=True, related_name='cabinets',
verbose_name=_('Documents')
)
class Meta:
ordering = ('parent__label', 'label')
# unique_together doesn't work if there is a FK
# https://code.djangoproject.com/ticket/1751
unique_together = ('parent', 'label')
verbose_name = _('Cabinet')
verbose_name_plural = _('Cabinets')
def __str__(self):
return self.get_full_path()
def get_absolute_url(self):
return reverse('cabinets:cabinet_view', args=(self.pk,))
def get_document_count(self, user):
return self.get_documents_queryset(user=user).count()
def get_documents_queryset(self, user):
return AccessControlList.objects.filter_by_access(
permission_document_view, user, queryset=self.documents
)
def get_full_path(self):
result = []
for node in self.get_ancestors(include_self=True):
result.append(node.label)
return ' / '.join(result)
def validate_unique(self, exclude=None):
# Explicit validation of uniqueness of parent+label as the provided
# unique_together check in Meta is not working for all 100% cases
# when there is a FK in the unique_together tuple
# https://code.djangoproject.com/ticket/1751
with transaction.atomic():
if Cabinet.objects.select_for_update().filter(parent=self.parent, label=self.label).exists():
params = {
'model_name': _('Cabinet'),
'field_labels': _('Parent and Label')
}
raise ValidationError(
{
NON_FIELD_ERRORS: [
ValidationError(
message=_(
'%(model_name)s with this %(field_labels)s already '
'exists.'
), code='unique_together', params=params,
)
],
},
)
class DocumentCabinet(Cabinet):
class Meta:
proxy = True
verbose_name = _('Document cabinet')
verbose_name_plural = _('Document cabinets')

View File

@@ -0,0 +1,28 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from permissions import PermissionNamespace
namespace = PermissionNamespace('cabinets', _('Cabinets'))
# Translators: this refers to the permission that will allow users to add
# documents to cabinets.
permission_cabinet_add_document = namespace.add_permission(
name='cabinet_add_document', label=_('Add documents to cabinets')
)
permission_cabinet_create = namespace.add_permission(
name='cabinet_create', label=_('Create cabinets')
)
permission_cabinet_delete = namespace.add_permission(
name='cabinet_delete', label=_('Delete cabinets')
)
permission_cabinet_edit = namespace.add_permission(
name='cabinet_edit', label=_('Edit cabinets')
)
permission_cabinet_remove_document = namespace.add_permission(
name='cabinet_remove_document', label=_('Remove documents from cabinets')
)
permission_cabinet_view = namespace.add_permission(
name='cabinet_view', label=_('View cabinets')
)

View File

@@ -0,0 +1,194 @@
from __future__ import unicode_literals
from django.db import transaction
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.settings import api_settings
from rest_framework_recursive.fields import RecursiveField
from documents.models import Document
from documents.serializers import DocumentSerializer
from .models import Cabinet
class CabinetSerializer(serializers.ModelSerializer):
children = RecursiveField(
help_text=_('List of children cabinets.'), many=True, read_only=True
)
documents_count = serializers.SerializerMethodField(
help_text=_('Number of documents on this cabinet level.')
)
full_path = serializers.SerializerMethodField(
help_text=_(
'The name of this cabinet level appended to the names of its '
'ancestors.'
)
)
documents_url = serializers.HyperlinkedIdentityField(
help_text=_(
'URL of the API endpoint showing the list documents inside this '
'cabinet.'
), view_name='rest_api:cabinet-document-list'
)
parent_url = serializers.SerializerMethodField()
class Meta:
extra_kwargs = {
'url': {'view_name': 'rest_api:cabinet-detail'},
}
fields = (
'children', 'documents_count', 'documents_url', 'full_path', 'id',
'label', 'parent', 'parent_url', 'url'
)
model = Cabinet
def get_documents_count(self, obj):
return obj.get_document_count(user=self.context['request'].user)
def get_full_path(self, obj):
return obj.get_full_path()
def get_parent_url(self, obj):
if obj.parent:
return reverse(
'rest_api:cabinet-detail', args=(obj.parent.pk,),
format=self.context['format'],
request=self.context.get('request')
)
else:
return ''
class WritableCabinetSerializer(serializers.ModelSerializer):
documents_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of document primary keys to add to this '
'cabinet.'
), required=False
)
# This is here because parent is optional in the model but the serializer
# sets it as required.
parent = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=Cabinet.objects.all(), required=False
)
class Meta:
fields = ('documents_pk_list', 'label', 'id', 'parent')
model = Cabinet
def _add_documents(self, documents_pk_list, instance):
instance.documents.add(
*Document.objects.filter(pk__in=documents_pk_list.split(','))
)
def create(self, validated_data):
documents_pk_list = validated_data.pop('documents_pk_list', '')
instance = super(WritableCabinetSerializer, self).create(validated_data)
if documents_pk_list:
self._add_documents(
documents_pk_list=documents_pk_list, instance=instance
)
return instance
def update(self, instance, validated_data):
documents_pk_list = validated_data.pop('documents_pk_list', '')
instance = super(WritableCabinetSerializer, self).update(
instance, validated_data
)
if documents_pk_list:
instance.documents.clear()
self._add_documents(
documents_pk_list=documents_pk_list, instance=instance
)
return instance
def run_validation(self, data=None):
# Copy data into a new dictionary since data is an immutable type
result = data.copy()
# Add None parent to keep validation from failing.
# This is here because parent is optional in the model but the serializer
# sets it as required.
result.setdefault('parent')
data = super(WritableCabinetSerializer, self).run_validation(result)
# Explicit validation of uniqueness of parent+label as the provided
# unique_together check in Meta is not working for all 100% cases
# when there is a FK in the unique_together tuple
# https://code.djangoproject.com/ticket/1751
with transaction.atomic():
if Cabinet.objects.select_for_update().filter(parent=data['parent'], label=data['label']).exists():
params = {
'model_name': _('Cabinet'),
'field_labels': _('Parent and Label')
}
raise serializers.ValidationError(
{
api_settings.NON_FIELD_ERRORS_KEY: [
_(
'%(model_name)s with this %(field_labels)s '
'already exists.'
) % params
],
},
)
return data
class CabinetDocumentSerializer(DocumentSerializer):
cabinet_document_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to a document in relation to the cabinet '
'storing it. This URL is different than the canonical document '
'URL.'
)
)
class Meta(DocumentSerializer.Meta):
fields = DocumentSerializer.Meta.fields + ('cabinet_document_url',)
read_only_fields = DocumentSerializer.Meta.fields
def get_cabinet_document_url(self, instance):
return reverse(
'rest_api:cabinet-document', args=(
self.context['cabinet'].pk, instance.pk
), request=self.context['request'], format=self.context['format']
)
class NewCabinetDocumentSerializer(serializers.Serializer):
documents_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of document primary keys to add to this '
'cabinet.'
)
)
def _add_documents(self, documents_pk_list, instance):
instance.documents.add(
*Document.objects.filter(pk__in=documents_pk_list.split(','))
)
def create(self, validated_data):
documents_pk_list = validated_data['documents_pk_list']
if documents_pk_list:
self._add_documents(
documents_pk_list=documents_pk_list,
instance=validated_data['cabinet']
)
return {'documents_pk_list': documents_pk_list}

View File

@@ -0,0 +1,13 @@
/debug
/jstree.sublime-project
/jstree.sublime-workspace
/bower_components
/node_modules
/site
/nuget
/demo/filebrowser/data/root
/npm.txt
/libs
/docs
/dist/libs
/.vscode

View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
{
"name": "jstree",
"version": "3.3.3",
"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"
]
}

View File

@@ -0,0 +1,28 @@
{
"name": "jstree",
"repo": "vakata/jstree",
"description": "jsTree is jquery plugin, that provides interactive trees.",
"version": "3.3.3",
"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"
}
}

View File

@@ -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"
]
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1 @@
[{"id":1,"text":"Root node","children":[{"id":2,"text":"Child node 1"},{"id":3,"text":"Child node 2"}]}]

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,450 @@
<?php
ini_set('open_basedir', dirname(__FILE__) . DIRECTORY_SEPARATOR);
class fs
{
protected $base = null;
protected function real($path) {
$temp = realpath($path);
if(!$temp) { throw new Exception('Path does not exist: ' . $path); }
if($this->base && strlen($this->base)) {
if(strpos($temp, $this->base) !== 0) { throw new Exception('Path is not inside base ('.$this->base.'): ' . $temp); }
}
return $temp;
}
protected function path($id) {
$id = str_replace('/', DIRECTORY_SEPARATOR, $id);
$id = trim($id, DIRECTORY_SEPARATOR);
$id = $this->real($this->base . DIRECTORY_SEPARATOR . $id);
return $id;
}
protected function id($path) {
$path = $this->real($path);
$path = substr($path, strlen($this->base));
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$path = trim($path, '/');
return strlen($path) ? $path : '/';
}
public function __construct($base) {
$this->base = $this->real($base);
if(!$this->base) { throw new Exception('Base directory does not exist'); }
}
public function lst($id, $with_root = false) {
$dir = $this->path($id);
$lst = @scandir($dir);
if(!$lst) { throw new Exception('Could not list path: ' . $dir); }
$res = array();
foreach($lst as $item) {
if($item == '.' || $item == '..' || $item === null) { continue; }
$tmp = preg_match('([^ a-zа-я-_0-9.]+)ui', $item);
if($tmp === false || $tmp === 1) { continue; }
if(is_dir($dir . DIRECTORY_SEPARATOR . $item)) {
$res[] = array('text' => $item, 'children' => true, 'id' => $this->id($dir . DIRECTORY_SEPARATOR . $item), 'icon' => 'folder');
}
else {
$res[] = array('text' => $item, 'children' => false, 'id' => $this->id($dir . DIRECTORY_SEPARATOR . $item), 'type' => 'file', 'icon' => 'file file-'.substr($item, strrpos($item,'.') + 1));
}
}
if($with_root && $this->id($dir) === '/') {
$res = array(array('text' => basename($this->base), 'children' => $res, 'id' => '/', 'icon'=>'folder', 'state' => array('opened' => true, 'disabled' => true)));
}
return $res;
}
public function data($id) {
if(strpos($id, ":")) {
$id = array_map(array($this, 'id'), explode(':', $id));
return array('type'=>'multiple', 'content'=> 'Multiple selected: ' . implode(' ', $id));
}
$dir = $this->path($id);
if(is_dir($dir)) {
return array('type'=>'folder', 'content'=> $id);
}
if(is_file($dir)) {
$ext = strpos($dir, '.') !== FALSE ? substr($dir, strrpos($dir, '.') + 1) : '';
$dat = array('type' => $ext, 'content' => '');
switch($ext) {
case 'txt':
case 'text':
case 'md':
case 'js':
case 'json':
case 'css':
case 'html':
case 'htm':
case 'xml':
case 'c':
case 'cpp':
case 'h':
case 'sql':
case 'log':
case 'py':
case 'rb':
case 'htaccess':
case 'php':
$dat['content'] = file_get_contents($dir);
break;
case 'jpg':
case 'jpeg':
case 'gif':
case 'png':
case 'bmp':
$dat['content'] = 'data:'.finfo_file(finfo_open(FILEINFO_MIME_TYPE), $dir).';base64,'.base64_encode(file_get_contents($dir));
break;
default:
$dat['content'] = 'File not recognized: '.$this->id($dir);
break;
}
return $dat;
}
throw new Exception('Not a valid selection: ' . $dir);
}
public function create($id, $name, $mkdir = false) {
$dir = $this->path($id);
if(preg_match('([^ a-zа-я-_0-9.]+)ui', $name) || !strlen($name)) {
throw new Exception('Invalid name: ' . $name);
}
if($mkdir) {
mkdir($dir . DIRECTORY_SEPARATOR . $name);
}
else {
file_put_contents($dir . DIRECTORY_SEPARATOR . $name, '');
}
return array('id' => $this->id($dir . DIRECTORY_SEPARATOR . $name));
}
public function rename($id, $name) {
$dir = $this->path($id);
if($dir === $this->base) {
throw new Exception('Cannot rename root');
}
if(preg_match('([^ a-zа-я-_0-9.]+)ui', $name) || !strlen($name)) {
throw new Exception('Invalid name: ' . $name);
}
$new = explode(DIRECTORY_SEPARATOR, $dir);
array_pop($new);
array_push($new, $name);
$new = implode(DIRECTORY_SEPARATOR, $new);
if($dir !== $new) {
if(is_file($new) || is_dir($new)) { throw new Exception('Path already exists: ' . $new); }
rename($dir, $new);
}
return array('id' => $this->id($new));
}
public function remove($id) {
$dir = $this->path($id);
if($dir === $this->base) {
throw new Exception('Cannot remove root');
}
if(is_dir($dir)) {
foreach(array_diff(scandir($dir), array(".", "..")) as $f) {
$this->remove($this->id($dir . DIRECTORY_SEPARATOR . $f));
}
rmdir($dir);
}
if(is_file($dir)) {
unlink($dir);
}
return array('status' => 'OK');
}
public function move($id, $par) {
$dir = $this->path($id);
$par = $this->path($par);
$new = explode(DIRECTORY_SEPARATOR, $dir);
$new = array_pop($new);
$new = $par . DIRECTORY_SEPARATOR . $new;
rename($dir, $new);
return array('id' => $this->id($new));
}
public function copy($id, $par) {
$dir = $this->path($id);
$par = $this->path($par);
$new = explode(DIRECTORY_SEPARATOR, $dir);
$new = array_pop($new);
$new = $par . DIRECTORY_SEPARATOR . $new;
if(is_file($new) || is_dir($new)) { throw new Exception('Path already exists: ' . $new); }
if(is_dir($dir)) {
mkdir($new);
foreach(array_diff(scandir($dir), array(".", "..")) as $f) {
$this->copy($this->id($dir . DIRECTORY_SEPARATOR . $f), $this->id($new));
}
}
if(is_file($dir)) {
copy($dir, $new);
}
return array('id' => $this->id($new));
}
}
if(isset($_GET['operation'])) {
$fs = new fs(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'root' . DIRECTORY_SEPARATOR);
try {
$rslt = null;
switch($_GET['operation']) {
case 'get_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$rslt = $fs->lst($node, (isset($_GET['id']) && $_GET['id'] === '#'));
break;
case "get_content":
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$rslt = $fs->data($node);
break;
case 'create_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$rslt = $fs->create($node, isset($_GET['text']) ? $_GET['text'] : '', (!isset($_GET['type']) || $_GET['type'] !== 'file'));
break;
case 'rename_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$rslt = $fs->rename($node, isset($_GET['text']) ? $_GET['text'] : '');
break;
case 'delete_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$rslt = $fs->remove($node);
break;
case 'move_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? $_GET['parent'] : '/';
$rslt = $fs->move($node, $parn);
break;
case 'copy_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
$parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? $_GET['parent'] : '/';
$rslt = $fs->copy($node, $parn);
break;
default:
throw new Exception('Unsupported operation: ' . $_GET['operation']);
break;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($rslt);
}
catch (Exception $e) {
header($_SERVER["SERVER_PROTOCOL"] . ' 500 Server Error');
header('Status: 500 Server Error');
echo $e->getMessage();
}
die();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Title</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="./../../dist/themes/default/style.min.css" />
<style>
html, body { background:#ebebeb; font-size:10px; font-family:Verdana; margin:0; padding:0; }
#container { min-width:320px; margin:0px auto 0 auto; background:white; border-radius:0px; padding:0px; overflow:hidden; }
#tree { float:left; min-width:319px; border-right:1px solid silver; overflow:auto; padding:0px 0; }
#data { margin-left:320px; }
#data textarea { margin:0; padding:0; height:100%; width:100%; border:0; background:white; display:block; line-height:18px; resize:none; }
#data, #code { font: normal normal normal 12px/18px 'Consolas', monospace !important; }
#tree .folder { background:url('./file_sprite.png') right bottom no-repeat; }
#tree .file { background:url('./file_sprite.png') 0 0 no-repeat; }
#tree .file-pdf { background-position: -32px 0 }
#tree .file-as { background-position: -36px 0 }
#tree .file-c { background-position: -72px -0px }
#tree .file-iso { background-position: -108px -0px }
#tree .file-htm, #tree .file-html, #tree .file-xml, #tree .file-xsl { background-position: -126px -0px }
#tree .file-cf { background-position: -162px -0px }
#tree .file-cpp { background-position: -216px -0px }
#tree .file-cs { background-position: -236px -0px }
#tree .file-sql { background-position: -272px -0px }
#tree .file-xls, #tree .file-xlsx { background-position: -362px -0px }
#tree .file-h { background-position: -488px -0px }
#tree .file-crt, #tree .file-pem, #tree .file-cer { background-position: -452px -18px }
#tree .file-php { background-position: -108px -18px }
#tree .file-jpg, #tree .file-jpeg, #tree .file-png, #tree .file-gif, #tree .file-bmp { background-position: -126px -18px }
#tree .file-ppt, #tree .file-pptx { background-position: -144px -18px }
#tree .file-rb { background-position: -180px -18px }
#tree .file-text, #tree .file-txt, #tree .file-md, #tree .file-log, #tree .file-htaccess { background-position: -254px -18px }
#tree .file-doc, #tree .file-docx { background-position: -362px -18px }
#tree .file-zip, #tree .file-gz, #tree .file-tar, #tree .file-rar { background-position: -416px -18px }
#tree .file-js { background-position: -434px -18px }
#tree .file-css { background-position: -144px -0px }
#tree .file-fla { background-position: -398px -0px }
</style>
</head>
<body>
<div id="container" role="main">
<div id="tree"></div>
<div id="data">
<div class="content code" style="display:none;"><textarea id="code" readonly="readonly"></textarea></div>
<div class="content folder" style="display:none;"></div>
<div class="content image" style="display:none; position:relative;"><img src="" alt="" style="display:block; position:absolute; left:50%; top:50%; padding:0; max-height:90%; max-width:90%;" /></div>
<div class="content default" style="text-align:center;">Select a file from the tree.</div>
</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="./../../dist/jstree.min.js"></script>
<script>
$(function () {
$(window).resize(function () {
var h = Math.max($(window).height() - 0, 420);
$('#container, #data, #tree, #data .content').height(h).filter('.default').css('lineHeight', h + 'px');
}).resize();
$('#tree')
.jstree({
'core' : {
'data' : {
'url' : '?operation=get_node',
'data' : function (node) {
return { 'id' : node.id };
}
},
'check_callback' : function(o, n, p, i, m) {
if(m && m.dnd && m.pos !== 'i') { return false; }
if(o === "move_node" || o === "copy_node") {
if(this.get_node(n).parent === this.get_node(p).id) { return false; }
}
return true;
},
'force_text' : true,
'themes' : {
'responsive' : false,
'variant' : 'small',
'stripes' : true
}
},
'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) ? 1 : -1);
},
'contextmenu' : {
'items' : function(node) {
var tmp = $.jstree.defaults.contextmenu.items();
delete tmp.create.action;
tmp.create.label = "New";
tmp.create.submenu = {
"create_folder" : {
"separator_after" : true,
"label" : "Folder",
"action" : function (data) {
var inst = $.jstree.reference(data.reference),
obj = inst.get_node(data.reference);
inst.create_node(obj, { type : "default" }, "last", function (new_node) {
setTimeout(function () { inst.edit(new_node); },0);
});
}
},
"create_file" : {
"label" : "File",
"action" : function (data) {
var inst = $.jstree.reference(data.reference),
obj = inst.get_node(data.reference);
inst.create_node(obj, { type : "file" }, "last", function (new_node) {
setTimeout(function () { inst.edit(new_node); },0);
});
}
}
};
if(this.get_type(node) === "file") {
delete tmp.create;
}
return tmp;
}
},
'types' : {
'default' : { 'icon' : 'folder' },
'file' : { 'valid_children' : [], 'icon' : 'file' }
},
'unique' : {
'duplicate' : function (name, counter) {
return name + ' ' + counter;
}
},
'plugins' : ['state','dnd','sort','types','contextmenu','unique']
})
.on('delete_node.jstree', function (e, data) {
$.get('?operation=delete_node', { 'id' : data.node.id })
.fail(function () {
data.instance.refresh();
});
})
.on('create_node.jstree', function (e, data) {
$.get('?operation=create_node', { 'type' : data.node.type, 'id' : data.node.parent, 'text' : data.node.text })
.done(function (d) {
data.instance.set_id(data.node, d.id);
})
.fail(function () {
data.instance.refresh();
});
})
.on('rename_node.jstree', function (e, data) {
$.get('?operation=rename_node', { 'id' : data.node.id, 'text' : data.text })
.done(function (d) {
data.instance.set_id(data.node, d.id);
})
.fail(function () {
data.instance.refresh();
});
})
.on('move_node.jstree', function (e, data) {
$.get('?operation=move_node', { 'id' : data.node.id, 'parent' : data.parent })
.done(function (d) {
//data.instance.load_node(data.parent);
data.instance.refresh();
})
.fail(function () {
data.instance.refresh();
});
})
.on('copy_node.jstree', function (e, data) {
$.get('?operation=copy_node', { 'id' : data.original.id, 'parent' : data.parent })
.done(function (d) {
//data.instance.load_node(data.parent);
data.instance.refresh();
})
.fail(function () {
data.instance.refresh();
});
})
.on('changed.jstree', function (e, data) {
if(data && data.selected && data.selected.length) {
$.get('?operation=get_content&id=' + data.selected.join(':'), function (d) {
if(d && typeof d.type !== 'undefined') {
$('#data .content').hide();
switch(d.type) {
case 'text':
case 'txt':
case 'md':
case 'htaccess':
case 'log':
case 'sql':
case 'php':
case 'js':
case 'json':
case 'css':
case 'html':
$('#data .code').show();
$('#code').val(d.content);
break;
case 'png':
case 'jpg':
case 'jpeg':
case 'bmp':
case 'gif':
$('#data .image img').one('load', function () { $(this).css({'marginTop':'-' + $(this).height()/2 + 'px','marginLeft':'-' + $(this).width()/2 + 'px'}); }).attr('src',d.content);
$('#data .image').show();
break;
default:
$('#data .default').html(d.content).show();
break;
}
}
});
}
else {
$('#data .content').hide();
$('#data .default').html('Select a file from the tree.').show();
}
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
-- phpMyAdmin SQL Dump
-- version 4.0.1
-- http://www.phpmyadmin.net
--
-- Host: 127.0.0.1
-- Generation Time: Apr 15, 2014 at 05:14 PM
-- Server version: 5.5.27
-- PHP Version: 5.4.7
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
--
-- Database: `test`
--
-- --------------------------------------------------------
--
-- Table structure for table `tree_data`
--
CREATE TABLE IF NOT EXISTS `tree_data` (
`id` int(10) unsigned NOT NULL,
`nm` varchar(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `tree_data`
--
INSERT INTO `tree_data` (`id`, `nm`) VALUES
(1, 'root'),
(1063, 'Node 12'),
(1064, 'Node 2'),
(1065, 'Node 3'),
(1066, 'Node 4'),
(1067, 'Node 5'),
(1068, 'Node 6'),
(1069, 'Node 7'),
(1070, 'Node 8'),
(1071, 'Node 9'),
(1072, 'Node 9'),
(1073, 'Node 9'),
(1074, 'Node 9'),
(1075, 'Node 7'),
(1076, 'Node 8'),
(1077, 'Node 9'),
(1078, 'Node 9'),
(1079, 'Node 9');
-- --------------------------------------------------------
--
-- Table structure for table `tree_struct`
--
CREATE TABLE IF NOT EXISTS `tree_struct` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`lft` int(10) unsigned NOT NULL,
`rgt` int(10) unsigned NOT NULL,
`lvl` int(10) unsigned NOT NULL,
`pid` int(10) unsigned NOT NULL,
`pos` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1083 ;
--
-- Dumping data for table `tree_struct`
--
INSERT INTO `tree_struct` (`id`, `lft`, `rgt`, `lvl`, `pid`, `pos`) VALUES
(1, 1, 36, 0, 0, 0),
(1063, 2, 31, 1, 1, 0),
(1064, 3, 30, 2, 1063, 0),
(1065, 4, 29, 3, 1064, 0),
(1066, 5, 28, 4, 1065, 0),
(1067, 6, 19, 5, 1066, 0),
(1068, 7, 18, 6, 1067, 0),
(1069, 8, 17, 7, 1068, 0),
(1070, 9, 16, 8, 1069, 0),
(1071, 12, 13, 9, 1070, 1),
(1072, 14, 15, 9, 1070, 2),
(1073, 10, 11, 9, 1070, 0),
(1074, 32, 35, 1, 1, 1),
(1075, 20, 27, 5, 1066, 1),
(1076, 21, 26, 6, 1075, 0),
(1077, 24, 25, 7, 1076, 1),
(1078, 33, 34, 2, 1074, 0),
(1079, 22, 23, 7, 1076, 0);

View File

@@ -0,0 +1,172 @@
<?php
require_once(dirname(__FILE__) . '/class.db.php');
require_once(dirname(__FILE__) . '/class.tree.php');
if(isset($_GET['operation'])) {
$fs = new tree(db::get('mysqli://root@127.0.0.1/test'), array('structure_table' => 'tree_struct', 'data_table' => 'tree_data', 'data' => array('nm')));
try {
$rslt = null;
switch($_GET['operation']) {
case 'analyze':
var_dump($fs->analyze(true));
die();
break;
case 'get_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$temp = $fs->get_children($node);
$rslt = array();
foreach($temp as $v) {
$rslt[] = array('id' => $v['id'], 'text' => $v['nm'], 'children' => ($v['rgt'] - $v['lft'] > 1));
}
break;
case "get_content":
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : 0;
$node = explode(':', $node);
if(count($node) > 1) {
$rslt = array('content' => 'Multiple selected');
}
else {
$temp = $fs->get_node((int)$node[0], array('with_path' => true));
$rslt = array('content' => 'Selected: /' . implode('/',array_map(function ($v) { return $v['nm']; }, $temp['path'])). '/'.$temp['nm']);
}
break;
case 'create_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$temp = $fs->mk($node, isset($_GET['position']) ? (int)$_GET['position'] : 0, array('nm' => isset($_GET['text']) ? $_GET['text'] : 'New node'));
$rslt = array('id' => $temp);
break;
case 'rename_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$rslt = $fs->rn($node, array('nm' => isset($_GET['text']) ? $_GET['text'] : 'Renamed node'));
break;
case 'delete_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$rslt = $fs->rm($node);
break;
case 'move_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? (int)$_GET['parent'] : 0;
$rslt = $fs->mv($node, $parn, isset($_GET['position']) ? (int)$_GET['position'] : 0);
break;
case 'copy_node':
$node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0;
$parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? (int)$_GET['parent'] : 0;
$rslt = $fs->cp($node, $parn, isset($_GET['position']) ? (int)$_GET['position'] : 0);
break;
default:
throw new Exception('Unsupported operation: ' . $_GET['operation']);
break;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($rslt);
}
catch (Exception $e) {
header($_SERVER["SERVER_PROTOCOL"] . ' 500 Server Error');
header('Status: 500 Server Error');
echo $e->getMessage();
}
die();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Title</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="./../../dist/themes/default/style.min.css" />
<style>
html, body { background:#ebebeb; font-size:10px; font-family:Verdana; margin:0; padding:0; }
#container { min-width:320px; margin:0px auto 0 auto; background:white; border-radius:0px; padding:0px; overflow:hidden; }
#tree { float:left; min-width:319px; border-right:1px solid silver; overflow:auto; padding:0px 0; }
#data { margin-left:320px; }
#data textarea { margin:0; padding:0; height:100%; width:100%; border:0; background:white; display:block; line-height:18px; }
#data, #code { font: normal normal normal 12px/18px 'Consolas', monospace !important; }
</style>
</head>
<body>
<div id="container" role="main">
<div id="tree"></div>
<div id="data">
<div class="content code" style="display:none;"><textarea id="code" readonly="readonly"></textarea></div>
<div class="content folder" style="display:none;"></div>
<div class="content image" style="display:none; position:relative;"><img src="" alt="" style="display:block; position:absolute; left:50%; top:50%; padding:0; max-height:90%; max-width:90%;" /></div>
<div class="content default" style="text-align:center;">Select a node from the tree.</div>
</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="./../../dist/jstree.min.js"></script>
<script>
$(function () {
$(window).resize(function () {
var h = Math.max($(window).height() - 0, 420);
$('#container, #data, #tree, #data .content').height(h).filter('.default').css('lineHeight', h + 'px');
}).resize();
$('#tree')
.jstree({
'core' : {
'data' : {
'url' : '?operation=get_node',
'data' : function (node) {
return { 'id' : node.id };
}
},
'check_callback' : true,
'themes' : {
'responsive' : false
}
},
'force_text' : true,
'plugins' : ['state','dnd','contextmenu','wholerow']
})
.on('delete_node.jstree', function (e, data) {
$.get('?operation=delete_node', { 'id' : data.node.id })
.fail(function () {
data.instance.refresh();
});
})
.on('create_node.jstree', function (e, data) {
$.get('?operation=create_node', { 'id' : data.node.parent, 'position' : data.position, 'text' : data.node.text })
.done(function (d) {
data.instance.set_id(data.node, d.id);
})
.fail(function () {
data.instance.refresh();
});
})
.on('rename_node.jstree', function (e, data) {
$.get('?operation=rename_node', { 'id' : data.node.id, 'text' : data.text })
.fail(function () {
data.instance.refresh();
});
})
.on('move_node.jstree', function (e, data) {
$.get('?operation=move_node', { 'id' : data.node.id, 'parent' : data.parent, 'position' : data.position })
.fail(function () {
data.instance.refresh();
});
})
.on('copy_node.jstree', function (e, data) {
$.get('?operation=copy_node', { 'id' : data.original.id, 'parent' : data.parent, 'position' : data.position })
.always(function () {
data.instance.refresh();
});
})
.on('changed.jstree', function (e, data) {
if(data && data.selected && data.selected.length) {
$.get('?operation=get_content&id=' + data.selected.join(':'), function (d) {
$('#data .default').text(d.content).show();
});
}
else {
$('#data .content').hide();
$('#data .default').text('Select a file from the tree.').show();
}
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,241 @@
/*global module:false, require:false, __dirname:false*/
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") %> - (<%= _.pluck(pkg.licenses, "type").join(", ") %>) */\n',
preserveComments: false,
//sourceMap: "dist/jstree.min.map",
//sourceMappingURL: "jstree.min.map",
report: "min",
beautify: {
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;
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']);
};

View File

@@ -0,0 +1,28 @@
{
"name": "jstree",
"title": "jsTree",
"description": "Tree view for jQuery",
"version": "3.3.3",
"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"
}
}

View File

@@ -0,0 +1,58 @@
{
"name": "jstree",
"title": "jsTree",
"description": "jQuery tree plugin",
"version": "3.3.3",
"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.4.4",
"grunt": "~0.4.0",
"grunt-contrib-concat": "*",
"grunt-contrib-copy": "*",
"grunt-contrib-imagemin": "~0.4.0",
"grunt-contrib-jshint": "*",
"grunt-contrib-less": "~0.8.2",
"grunt-contrib-qunit": "~v0.3.0",
"grunt-contrib-uglify": "*",
"grunt-contrib-watch": "~0.5.3",
"grunt-phantomcss-gitdiff": "0.0.7",
"grunt-resemble-cli": "0.0.8",
"grunt-text-replace": "~0.3.11"
},
"dependencies": {
"jquery": ">=1.9.1"
},
"npmName": "jstree",
"npmFileMap": [
{
"basePath": "/dist/",
"files": [
"jstree.min.js",
"themes/**/*.png",
"themes/**/*.gif",
"themes/**/*.min.css"
]
}
]
}

View File

@@ -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";

View File

@@ -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);
};
};
}));

File diff suppressed because it is too large Load Diff

View File

@@ -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)) {
parent.activate_node.call(this, obj, e);
}
};
};
}));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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);
};
};
}));

View File

@@ -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");
}));

View File

@@ -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");
}));

View File

@@ -0,0 +1,125 @@
/**
* ### 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
};
$.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 st = { 'state' : this.get_state(), '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) {
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");
}));

View File

@@ -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");
}));

View File

@@ -0,0 +1,121 @@
/**
* ### 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,
/**
* 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,
m = this._model.data, i, j;
for(i = 0, j = par.children.length; i < j; i++) {
c.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
}
if(!s) { n = n.toLowerCase(); }
switch(chk) {
case "delete_node":
return true;
case "rename_node":
i = ($.inArray(n, c) === -1 || (obj.text && obj.text[ s ? 'toString' : 'toLowerCase']() === 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, cb = this.settings.unique.duplicate;
n = tmp = this.get_string('New node');
dpc = [];
for(i = 0, j = par.children.length; i < j; i++) {
dpc.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
}
i = 1;
while($.inArray(s ? n : n.toLowerCase(), dpc) !== -1) {
n = cb.call(this, tmp, (++i)).toString();
}
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");
}));

View File

@@ -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 = '&#160;';
$.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");
}));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
}));

View File

@@ -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);

View File

@@ -0,0 +1,88 @@
// 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
.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; }
}
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; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -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; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
/* jsTree default theme */
@theme-name: default;
@hovered-bg-color: #e7f4f9;
@hovered-shadow-color: #cccccc;
@disabled-color: #666666;
@disabled-bg-color: #efefef;
@clicked-bg-color: #beebff;
@clicked-shadow-color: #999999;
@clicked-gradient-color-1: #beebff;
@clicked-gradient-color-2: #a8e4ff;
@search-result-color: #8b0000;
@mobile-wholerow-bg-color: #ebebeb;
@mobile-wholerow-shadow: #666666;
@mobile-wholerow-bordert: rgba(255,255,255,0.7);
@mobile-wholerow-borderb: rgba(64,64,64,0.2);
@responsive: true;
@image-path: "";
@base-height: 40px;
@import "../mixins.less";
@import "../base.less";
@import "../main.less";

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,77 @@
.jstree-@{theme-name} {
.jstree-node,
.jstree-icon { background-repeat:no-repeat; background-color:transparent; }
.jstree-anchor,
.jstree-animated,
.jstree-wholerow { transition:background-color 0.15s, box-shadow 0.15s; }
.jstree-hovered { background:@hovered-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @hovered-shadow-color; }
.jstree-context { background:@hovered-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @hovered-shadow-color; }
.jstree-clicked { background:@clicked-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @clicked-shadow-color; }
.jstree-no-icons .jstree-anchor > .jstree-themeicon { display:none; }
.jstree-disabled {
background:transparent; color:@disabled-color;
&.jstree-hovered { background:transparent; box-shadow:none; }
&.jstree-clicked { background:@disabled-bg-color; }
> .jstree-icon { opacity:0.8; filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'jstree-grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#jstree-grayscale"); /* Firefox 10+ */ filter: gray; /* IE6-9 */ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */ }
}
// search
.jstree-search { font-style:italic; color:@search-result-color; font-weight:bold; }
// checkboxes
.jstree-no-checkboxes .jstree-checkbox { display:none !important; }
&.jstree-checkbox-no-clicked {
.jstree-clicked {
background:transparent;
box-shadow:none;
&.jstree-hovered { background:@hovered-bg-color; }
}
> .jstree-wholerow-ul .jstree-wholerow-clicked {
background:transparent;
&.jstree-wholerow-hovered { background:@hovered-bg-color; }
}
}
// stripes
> .jstree-striped { min-width:100%; display:inline-block; background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB/qqA+AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMNAMM9s3UAAAAXSURBVHjajcEBAQAAAIKg/H/aCQZ70AUBjAATb6YPDgAAAABJRU5ErkJggg==") left top repeat; }
// wholerow
> .jstree-wholerow-ul .jstree-hovered,
> .jstree-wholerow-ul .jstree-clicked { background:transparent; box-shadow:none; border-radius:0; }
.jstree-wholerow { -moz-box-sizing:border-box; -webkit-box-sizing:border-box; box-sizing:border-box; }
.jstree-wholerow-hovered { background:@hovered-bg-color; }
.jstree-wholerow-clicked { .gradient(@clicked-gradient-color-1, @clicked-gradient-color-2); }
}
// theme variants
.jstree-@{theme-name} {
.jstree-theme(24px, "@{image-path}32px.png", 32px);
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); }
&.jstree-rtl .jstree-last { background:transparent; }
}
.jstree-@{theme-name}-small {
.jstree-theme(18px, "@{image-path}32px.png", 32px);
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); }
&.jstree-rtl .jstree-last { background:transparent; }
}
.jstree-@{theme-name}-large {
.jstree-theme(32px, "@{image-path}32px.png", 32px);
&.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); }
&.jstree-rtl .jstree-last { background:transparent; }
}
// mobile theme attempt
@media (max-width: 768px) {
#jstree-dnd.jstree-dnd-responsive when (@responsive = true) {
line-height:@base-height; font-weight:bold; font-size:1.1em; text-shadow:1px 1px white;
> i { background:transparent; width:@base-height; height:@base-height; }
> .jstree-ok { background-image:url("@{image-path}@{base-height}.png"); background-position:0 -(@base-height * 5); background-size:(@base-height * 3) (@base-height * 6); }
> .jstree-er { background-image:url("@{image-path}@{base-height}.png"); background-position:-(@base-height * 1) -(@base-height * 5); background-size:(@base-height * 3) (@base-height * 6); }
}
#jstree-marker.jstree-dnd-responsive when (@responsive = true) {
border-left-width:10px;
border-top-width:10px;
border-bottom-width:10px;
margin-top:-10px;
}
}
.jstree-@{theme-name}-responsive when (@responsive = true) {
@import "responsive.less";
}

View File

@@ -0,0 +1,105 @@
.gradient (@color1; @color2) {
background:@color1;
background: -webkit-linear-gradient(top, @color1 0%,@color2 100%);
background: linear-gradient(to bottom, @color1 0%,@color2 100%);
}
.jstree-theme (@base-height, @image, @image-height) {
@correction: (@image-height - @base-height) / 2;
.jstree-node { min-height:@base-height; line-height:@base-height; margin-left:@base-height; min-width:@base-height; }
.jstree-anchor { line-height:@base-height; height:@base-height; }
.jstree-icon { width:@base-height; height:@base-height; line-height:@base-height; }
.jstree-icon:empty { width:@base-height; height:@base-height; line-height:@base-height; }
&.jstree-rtl .jstree-node { margin-right:@base-height; }
.jstree-wholerow { height:@base-height; }
.jstree-node,
.jstree-icon { background-image:url("@{image}"); }
.jstree-node { background-position:-(@image-height * 9 + @correction) -@correction; background-repeat:repeat-y; }
.jstree-last { background:transparent; }
.jstree-open > .jstree-ocl { background-position:-(@image-height * 4 + @correction) -@correction; }
.jstree-closed > .jstree-ocl { background-position:-(@image-height * 3 + @correction) -@correction; }
.jstree-leaf > .jstree-ocl { background-position:-(@image-height * 2 + @correction) -@correction; }
.jstree-themeicon { background-position:-(@image-height * 8 + @correction) -@correction; }
> .jstree-no-dots {
.jstree-node,
.jstree-leaf > .jstree-ocl { background:transparent; }
.jstree-open > .jstree-ocl { background-position:-(@image-height * 1 + @correction) -@correction; }
.jstree-closed > .jstree-ocl { background-position:-@correction -@correction; }
}
.jstree-disabled {
background:transparent;
&.jstree-hovered {
background:transparent;
}
&.jstree-clicked {
background:#efefef;
}
}
.jstree-checkbox {
background-position:-(@image-height * 5 + @correction) -@correction;
&:hover { background-position:-(@image-height * 5 + @correction) -(@image-height * 1 + @correction); }
}
&.jstree-checkbox-selection .jstree-clicked, .jstree-checked {
> .jstree-checkbox {
background-position:-(@image-height * 7 + @correction) -@correction;
&:hover { background-position:-(@image-height * 7 + @correction) -(@image-height * 1 + @correction); }
}
}
.jstree-anchor {
> .jstree-undetermined {
background-position:-(@image-height * 6 + @correction) -@correction;
&:hover {
background-position:-(@image-height * 6 + @correction) -(@image-height * 1 + @correction);
}
}
}
.jstree-checkbox-disabled { opacity:0.8; filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'jstree-grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#jstree-grayscale"); /* Firefox 10+ */ filter: gray; /* IE6-9 */ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */ }
> .jstree-striped { background-size:auto (@base-height * 2); }
&.jstree-rtl {
.jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); background-position: 100% 1px; background-repeat:repeat-y; }
.jstree-last { background:transparent; }
.jstree-open > .jstree-ocl { background-position:-(@image-height * 4 + @correction) -(@image-height * 1 + @correction); }
.jstree-closed > .jstree-ocl { background-position:-(@image-height * 3 + @correction) -(@image-height * 1 + @correction); }
.jstree-leaf > .jstree-ocl { background-position:-(@image-height * 2 + @correction) -(@image-height * 1 + @correction); }
> .jstree-no-dots {
.jstree-node,
.jstree-leaf > .jstree-ocl { background:transparent; }
.jstree-open > .jstree-ocl { background-position:-(@image-height * 1 + @correction) -(@image-height * 1 + @correction); }
.jstree-closed > .jstree-ocl { background-position:-@correction -(@image-height * 1 + @correction); }
}
}
.jstree-themeicon-custom { background-color:transparent; background-image:none; background-position:0 0; }
> .jstree-container-ul .jstree-loading > .jstree-ocl { background:url("@{image-path}throbber.gif") center center no-repeat; }
.jstree-file { background:url("@{image}") -(@image-height * 3 + @correction) -(@image-height * 2 + @correction) no-repeat; }
.jstree-folder { background:url("@{image}") -(@image-height * 8 + @correction) -(@correction) no-repeat; }
> .jstree-container-ul > .jstree-node { margin-left:0; margin-right:0; }
// drag'n'drop
#jstree-dnd& {
line-height:@base-height; padding:0 4px;
.jstree-ok,
.jstree-er { background-image:url("@{image-path}32px.png"); background-repeat:no-repeat; background-color:transparent; }
i { background:transparent; width:@base-height; height:@base-height; line-height:@base-height; }
.jstree-ok { background-position: -(@correction) -(@image-height * 2 + @correction); }
.jstree-er { background-position: -(@image-height * 1 + @correction) -(@image-height * 2 + @correction); }
}
// ellipsis
.jstree-ellipsis { overflow: hidden; }
// base height + PADDINGS!
.jstree-ellipsis .jstree-anchor { width: calc(100% ~"-" (@base-height + 5px)); text-overflow: ellipsis; overflow: hidden; }
.jstree-ellipsis.jstree-no-icons .jstree-anchor { width: calc(100% ~"-" 5px); }
}

View File

@@ -0,0 +1,67 @@
@media (max-width: 768px) {
// background image
.jstree-icon { background-image:url("@{image-path}@{base-height}.png"); }
.jstree-node,
.jstree-leaf > .jstree-ocl { background:transparent; }
.jstree-node { min-height:@base-height; line-height:@base-height; margin-left:@base-height; min-width:@base-height; white-space:nowrap; }
.jstree-anchor { line-height:@base-height; height:@base-height; }
.jstree-icon, .jstree-icon:empty { width:@base-height; height:@base-height; line-height:@base-height; }
> .jstree-container-ul > .jstree-node { margin-left:0; }
&.jstree-rtl .jstree-node { margin-left:0; margin-right:@base-height; background:transparent; }
&.jstree-rtl .jstree-container-ul > .jstree-node { margin-right:0; }
.jstree-ocl,
.jstree-themeicon,
.jstree-checkbox { background-size:(@base-height * 3) (@base-height * 6); }
.jstree-leaf > .jstree-ocl,
&.jstree-rtl .jstree-leaf > .jstree-ocl { background:transparent; }
.jstree-open > .jstree-ocl { background-position:0 0px !important; }
.jstree-closed > .jstree-ocl { background-position:0 -(@base-height * 1) !important; }
&.jstree-rtl .jstree-closed > .jstree-ocl { background-position:-(@base-height * 1) 0px !important; }
.jstree-themeicon { background-position:-(@base-height * 1) -(@base-height * 1); }
.jstree-checkbox, .jstree-checkbox:hover { background-position:-(@base-height * 1) -(@base-height * 2); }
&.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox,
&.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover,
.jstree-checked > .jstree-checkbox,
.jstree-checked > .jstree-checkbox:hover { background-position:0 -(@base-height * 2); }
.jstree-anchor > .jstree-undetermined, .jstree-anchor > .jstree-undetermined:hover { background-position:0 -(@base-height * 3); }
.jstree-anchor { font-weight:bold; font-size:1.1em; text-shadow:1px 1px white; }
> .jstree-striped { background:transparent; }
.jstree-wholerow { border-top:1px solid @mobile-wholerow-bordert; border-bottom:1px solid @mobile-wholerow-borderb; background:@mobile-wholerow-bg-color; height:@base-height; }
.jstree-wholerow-hovered { background:@hovered-bg-color; }
.jstree-wholerow-clicked { background:@clicked-bg-color; }
// thanks to PHOTONUI
.jstree-children .jstree-last > .jstree-wholerow { box-shadow: inset 0 -6px 3px -5px @mobile-wholerow-shadow; }
.jstree-children .jstree-open > .jstree-wholerow { box-shadow: inset 0 6px 3px -5px @mobile-wholerow-shadow; border-top:0; }
.jstree-children .jstree-open + .jstree-open { box-shadow:none; }
// experiment
.jstree-node,
.jstree-icon,
.jstree-node > .jstree-ocl,
.jstree-themeicon,
.jstree-checkbox { background-image:url("@{image-path}@{base-height}.png"); background-size:(@base-height * 3) (@base-height * 6); }
.jstree-node { background-position:-(@base-height * 2) 0; background-repeat:repeat-y; }
.jstree-last { background:transparent; }
.jstree-leaf > .jstree-ocl { background-position:-(@base-height * 1) -(@base-height * 3); }
.jstree-last > .jstree-ocl { background-position:-(@base-height * 1) -(@base-height * 4); }
/*
.jstree-open > .jstree-ocl,
.jstree-closed > .jstree-ocl { border-radius:20px; background-color:white; }
*/
.jstree-themeicon-custom { background-color:transparent; background-image:none; background-position:0 0; }
.jstree-file { background:url("@{image-path}@{base-height}.png") 0 -(@base-height * 4) no-repeat; background-size:(@base-height * 3) (@base-height * 6); }
.jstree-folder { background:url("@{image-path}@{base-height}.png") -(@base-height * 1) -(@base-height * 1) no-repeat; background-size:(@base-height * 3) (@base-height * 6); }
> .jstree-container-ul > .jstree-node { margin-left:0; margin-right:0; }
}

View File

@@ -0,0 +1,38 @@
(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);
}
}(function ($, undefined) {
"use strict";
if(document.registerElement && Object && Object.create) {
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function () {
var c = { core : {}, plugins : [] }, i;
for(i in $.jstree.plugins) {
if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) {
c.plugins.push(i);
if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) {
c[i] = JSON.parse(this.getAttribute(i));
}
}
}
for(i in $.jstree.defaults.core) {
if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) {
c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i);
}
}
$(this).jstree(c);
};
// proto.attributeChangedCallback = function (name, previous, value) { };
try {
document.registerElement("vakata-jstree", { prototype: proto });
} catch(ignore) { }
}
}));

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Basic Test Suite</title>
<!-- Load local QUnit. -->
<link rel="stylesheet" href="libs/qunit.css" media="screen">
<script src="libs/qunit.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">this had better work.</div>
<!-- Load local lib and tests. -->
<script src="test.js"></script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
/**
* QUnit v1.12.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699a4;
background-color: #0d3349;
font-size: 1.5em;
line-height: 1em;
font-weight: normal;
border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
-webkit-border-top-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
}
#qunit-header a {
text-decoration: none;
color: #c2ccd1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #fff;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 .5em 0 .1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #eee;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #c2ccd1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 .5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #e0f2be;
color: #374e0c;
text-decoration: none;
}
#qunit-tests ins {
background-color: #ffcaca;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: black; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3c510c;
background-color: #fff;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
}
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: green; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-bottom: 1px solid white;
}
#qunit-testresult .module-name {
font-weight: bold;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
test('basic test', function() {
expect(1);
ok(true, 'this had better work.');
});
test('can access the DOM', function() {
expect(1);
var fixture = document.getElementById('qunit-fixture');
equal(fixture.innerText || fixture.textContent, 'this had better work.', 'should be able to access the DOM.');
});

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Light theme visual tests</title>
<link rel="stylesheet" href="./../../../dist/themes/default/style.min.css">
<link rel="stylesheet" href="./../../../dist/themes/default-dark/style.min.css">
<style>.tree { border:1px solid black; padding:10px; width:300px; margin:20px; float:left; min-height:200px; }</style>
</head>
<body style="background:white;">
<div class="tree" id="empty"></div>
<div class="tree" id="core"><ul><li>asdf</li></ul></div>
<div class="tree" id="tree">
<ul>
<li>Node 01
<ul>
<li>Node</li>
<li>Node</li>
</ul>
</li>
<li>Node 02</li>
<li data-jstree='{"opened" : true}'>Node 03
<ul>
<li>Node</li>
<li>Node</li>
</ul>
</li>
<li>Node 04</li>
<li>Node 05</li>
</ul>
</div>
<div class="tree" id="full"><ul><li data-jstree='{ "selected" : true, "type" : "file" }'>full</li><li>asdf</li></ul></div>
<div class="tree" id="dark"><ul><li data-jstree='{ "selected" : true, "type" : "file"}'>full</li><li>asdf</li></ul></div>
<script src="./../../../dist/libs/jquery.js"></script>
<script src="./../../../dist/jstree.min.js"></script>
<script>
$('#empty').jstree();
$('#tree, #core').jstree();
$('#full').jstree({ plugins : ["checkbox","sort","types","wholerow"], "types" : { "file" : { "icon" : "jstree-file" } } });
$('#dark').jstree({ plugins : ["checkbox","sort","types","wholerow"], "core" : { "themes" : { "name" : "default-dark" } }, "types" : { "file" : { "icon" : "jstree-file" } } });
</script>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mobile theme visual tests</title>
<link rel="stylesheet" href="./../../../dist/themes/default/style.min.css">
<link rel="stylesheet" href="./../../../dist/themes/default-dark/style.min.css">
<style>.tree { border:1px solid black; padding:10px; width:300px; margin:20px; float:left; min-height:200px; }</style>
</head>
<body style="background:white;">
<div class="tree" id="tree">
<ul>
<li>Node 01
<ul>
<li>Node</li>
<li>Node</li>
</ul>
</li>
<li>Node 02</li>
<li data-jstree='{"opened" : true}'>Node 03
<ul>
<li>Node</li>
<li>Node</li>
</ul>
</li>
<li>Node 04</li>
<li>Node 05</li>
</ul>
</div>
<div class="tree" id="full"><ul><li data-jstree='{ "selected" : true, "type" : "file" }'>full</li><li>asdf</li></ul></div>
<div class="tree" id="dark"><ul><li data-jstree='{ "selected" : true, "type" : "file"}'>full</li><li>asdf</li></ul></div>
<script src="./../../../dist/libs/jquery.js"></script>
<script src="./../../../dist/jstree.min.js"></script>
<script>
$.jstree.defaults.core.themes.responsive = true;
$('#tree').jstree();
$('#full').jstree({ plugins : ["checkbox","sort","types","wholerow"], "types" : { "file" : { "icon" : "jstree-file" } } });
$('#dark').jstree({ plugins : ["checkbox","sort","types","wholerow"], "core" : { "themes" : { "name" : "default-dark" } }, "types" : { "file" : { "icon" : "jstree-file" } } });
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,57 @@
{% extends 'appearance/base.html' %}
{% load i18n %}
{% load static %}
{% load navigation_tags %}
{% block title %}{% include 'appearance/calculate_form_title.html' %}{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{% static 'cabinets/packages/jstree/dist/themes/default/style.min.css' %}" />
{% endblock %}
{% block content %}
{% if title %}
<h3>{{ title }}</h3>
<hr>
{% endif %}
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-4">
<h4>{% trans 'Navigation:' %}</h4>
<div class="jstree"></div>
</div>
<div class="col-xs-12 col-sm-12 col-md-8">
{% with document_list as object_list %}
{% include 'appearance/generic_list_subtemplate.html' %}
{% endwith %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{% static 'cabinets/packages/jstree/dist/jstree.min.js' %}"></script>
<script>
$(function () {
var jstreeElement = $('.jstree');
jstreeElement
.on("select_node.jstree", function (e, data) {
if(data.selected.length) {
window.location.href=data.instance.get_node(data.selected[0]).data.href;
}
})
.jstree({
'core' : {
'data' : [
{{ jstree_data|safe }}
],
'themes' : {
'responsive' : true,
}
},
});
});
</script>
{% endblock %}

View File

View File

@@ -0,0 +1,5 @@
from __future__ import absolute_import, unicode_literals
TEST_CABINET_LABEL = 'test cabinet label'
TEST_CABINET_EDITED_LABEL = 'test cabinet edited label'

View File

@@ -0,0 +1,241 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import override_settings
from django.utils.encoding import force_text
from rest_framework.test import APITestCase
from documents.models import DocumentType
from documents.tests import TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH
from user_management.tests.literals import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME
)
from ..models import Cabinet
from .literals import TEST_CABINET_EDITED_LABEL, TEST_CABINET_LABEL
@override_settings(OCR_AUTO_OCR=False)
class CabinetAPITestCase(APITestCase):
"""
Test the cabinet API endpoints
"""
def setUp(self):
super(CabinetAPITestCase, self).setUp()
self.admin_user = get_user_model().objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
self.document_type = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(
file_object=file_object,
)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document_2 = self.document_type.new_document(
file_object=file_object,
)
def tearDown(self):
self.document_type.delete()
super(CabinetAPITestCase, self).tearDown()
def test_cabinet_create(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {'label': TEST_CABINET_LABEL}
)
cabinet = Cabinet.objects.first()
self.assertEqual(response.data['id'], cabinet.pk)
self.assertEqual(response.data['label'], TEST_CABINET_LABEL)
self.assertEqual(Cabinet.objects.count(), 1)
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_single_document(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {
'label': TEST_CABINET_LABEL, 'documents_pk_list': '{}'.format(
self.document.pk
)
}
)
cabinet = Cabinet.objects.first()
self.assertEqual(response.data['id'], cabinet.pk)
self.assertEqual(response.data['label'], TEST_CABINET_LABEL)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_multiple_documents(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {
'label': TEST_CABINET_LABEL,
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
)
}
)
cabinet = Cabinet.objects.first()
self.assertEqual(response.data['id'], cabinet.pk)
self.assertEqual(response.data['label'], TEST_CABINET_LABEL)
self.assertEqual(Cabinet.objects.count(), 1)
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
self.assertQuerysetEqual(
cabinet.documents.all(), map(
repr, (self.document, self.document_2)
)
)
def test_cabinet_document_delete(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
self.client.delete(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
)
self.assertEqual(cabinet.documents.count(), 0)
def test_cabinet_document_detail(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.client.get(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
)
self.assertEqual(response.data['uuid'], force_text(self.document.uuid))
def test_cabinet_document_list(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.client.get(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,))
)
self.assertEqual(
response.data['results'][0]['uuid'], force_text(self.document.uuid)
)
def test_cabinet_delete(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.client.delete(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,))
)
self.assertEqual(Cabinet.objects.count(), 0)
def test_cabinet_edit_via_patch(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.client.patch(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
)
cabinet.refresh_from_db()
self.assertEqual(cabinet.label, TEST_CABINET_EDITED_LABEL)
def test_cabinet_edit_via_put(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.client.put(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
)
cabinet.refresh_from_db()
self.assertEqual(cabinet.label, TEST_CABINET_EDITED_LABEL)
def test_cabinet_add_document(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
'documents_pk_list': '{}'.format(self.document.pk)
}
)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
def test_cabinet_add_multiple_documents(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
)
}
)
self.assertQuerysetEqual(
cabinet.documents.all(), map(
repr, (self.document, self.document_2)
)
)
def test_cabinet_list_view(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
Cabinet.objects.create(
label=TEST_CABINET_LABEL, parent=cabinet
)
response = self.client.get(
reverse('rest_api:cabinet-list')
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['results'][0]['label'], cabinet.label)
def test_cabinet_remove_document(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
self.client.delete(
reverse(
'rest_api:cabinet-document', args=(
cabinet.pk, self.document.pk
)
),
)
self.assertEqual(cabinet.documents.count(), 0)

View File

@@ -0,0 +1,82 @@
from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.test import override_settings
from common.tests import BaseTestCase
from documents.models import DocumentType
from documents.tests import TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH
from ..models import Cabinet
from .literals import TEST_CABINET_LABEL
@override_settings(OCR_AUTO_OCR=False)
class CabinetTestCase(BaseTestCase):
def setUp(self):
super(CabinetTestCase, self).setUp()
self.document_type = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(
file_object=file_object
)
def tearDown(self):
self.document_type.delete()
super(CabinetTestCase, self).tearDown()
def test_cabinet_creation(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.assertEqual(Cabinet.objects.all().count(), 1)
self.assertQuerysetEqual(Cabinet.objects.all(), (repr(cabinet),))
def test_cabinet_duplicate_creation(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
with self.assertRaises(ValidationError):
cabinet_2 = Cabinet(label=TEST_CABINET_LABEL)
cabinet_2.validate_unique()
cabinet_2.save()
self.assertEqual(Cabinet.objects.all().count(), 1)
self.assertQuerysetEqual(Cabinet.objects.all(), (repr(cabinet),))
def test_inner_cabinet_creation(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
inner_cabinet = Cabinet.objects.create(
parent=cabinet, label=TEST_CABINET_LABEL
)
self.assertEqual(Cabinet.objects.all().count(), 2)
self.assertQuerysetEqual(
Cabinet.objects.all(), map(repr, (cabinet, inner_cabinet))
)
def test_addition_of_documents(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
self.assertEqual(cabinet.documents.count(), 1)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
def test_addition_and_deletion_of_documents(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
self.assertEqual(cabinet.documents.count(), 1)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
cabinet.documents.remove(self.document)
self.assertEqual(cabinet.documents.count(), 0)
self.assertQuerysetEqual(cabinet.documents.all(), ())

View File

@@ -0,0 +1,207 @@
from __future__ import absolute_import, unicode_literals
from documents.permissions import permission_document_view
from documents.tests.test_views import GenericDocumentViewTestCase
from ..models import Cabinet
from ..permissions import (
permission_cabinet_add_document, permission_cabinet_create,
permission_cabinet_delete, permission_cabinet_edit,
permission_cabinet_remove_document, permission_cabinet_view
)
from .literals import TEST_CABINET_LABEL, TEST_CABINET_EDITED_LABEL
class CabinetViewTestCase(GenericDocumentViewTestCase):
def setUp(self):
super(CabinetViewTestCase, self).setUp()
self.login_user()
def _create_cabinet(self, label):
return self.post(
'cabinets:cabinet_create', data={
'label': TEST_CABINET_LABEL
}
)
def test_cabinet_create_view_no_permission(self):
response = self._create_cabinet(label=TEST_CABINET_LABEL)
self.assertEquals(response.status_code, 403)
self.assertEqual(Cabinet.objects.count(), 0)
def test_cabinet_create_view_with_permission(self):
self.grant(permission=permission_cabinet_create)
response = self._create_cabinet(label=TEST_CABINET_LABEL)
self.assertEqual(response.status_code, 302)
self.assertEqual(Cabinet.objects.count(), 1)
self.assertEqual(Cabinet.objects.first().label, TEST_CABINET_LABEL)
def test_cabinet_create_duplicate_view_with_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_create)
response = self._create_cabinet(label=TEST_CABINET_LABEL)
# HTTP 200 with error message
self.assertEqual(response.status_code, 200)
self.assertEqual(Cabinet.objects.count(), 1)
self.assertEqual(Cabinet.objects.first().pk, cabinet.pk)
def _delete_cabinet(self, cabinet):
return self.post('cabinets:cabinet_delete', args=(cabinet.pk,))
def test_cabinet_delete_view_no_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self._delete_cabinet(cabinet=cabinet)
self.assertEqual(response.status_code, 403)
self.assertEqual(Cabinet.objects.count(), 1)
def test_cabinet_delete_view_with_permission(self):
self.grant(permission=permission_cabinet_delete)
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self._delete_cabinet(cabinet=cabinet)
self.assertEqual(response.status_code, 302)
self.assertEqual(Cabinet.objects.count(), 0)
def _edit_cabinet(self, cabinet, label):
return self.post(
'cabinets:cabinet_edit', args=(cabinet.pk,), data={
'label': label
}
)
def test_cabinet_edit_view_no_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self._edit_cabinet(
cabinet=cabinet, label=TEST_CABINET_EDITED_LABEL
)
self.assertEqual(response.status_code, 403)
cabinet.refresh_from_db()
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_edit_view_with_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_edit)
response = self._edit_cabinet(
cabinet=cabinet, label=TEST_CABINET_EDITED_LABEL
)
self.assertEqual(response.status_code, 302)
cabinet.refresh_from_db()
self.assertEqual(cabinet.label, TEST_CABINET_EDITED_LABEL)
def _add_document_to_cabinet(self, cabinet):
return self.post(
'cabinets:cabinet_add_document', args=(self.document.pk,), data={
'cabinets': cabinet.pk
}
)
def test_cabinet_add_document_view_no_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_view)
response = self._add_document_to_cabinet(cabinet=cabinet)
self.assertContains(
response, text='Select a valid choice.', status_code=200
)
cabinet.refresh_from_db()
self.assertEqual(cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_view)
self.grant(permission=permission_cabinet_add_document)
self.grant(permission=permission_document_view)
response = self._add_document_to_cabinet(cabinet=cabinet)
cabinet.refresh_from_db()
self.assertEqual(response.status_code, 302)
self.assertEqual(cabinet.documents.count(), 1)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
def _add_multiple_documents_to_cabinet(self, cabinet):
return self.post(
'cabinets:cabinet_add_multiple_documents', data={
'id_list': (self.document.pk,), 'cabinets': cabinet.pk
}
)
def test_cabinet_add_multiple_documents_view_no_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_view)
response = self._add_multiple_documents_to_cabinet(cabinet=cabinet)
self.assertContains(
response, text='Select a valid choice', status_code=200
)
cabinet.refresh_from_db()
self.assertEqual(cabinet.documents.count(), 0)
def test_cabinet_add_multiple_documents_view_with_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
self.grant(permission=permission_cabinet_view)
self.grant(permission=permission_cabinet_add_document)
response = self._add_multiple_documents_to_cabinet(cabinet=cabinet)
self.assertEqual(response.status_code, 302)
cabinet.refresh_from_db()
self.assertEqual(cabinet.documents.count(), 1)
self.assertQuerysetEqual(
cabinet.documents.all(), (repr(self.document),)
)
def _remove_document_from_cabinet(self, cabinet):
return self.post(
'cabinets:document_cabinet_remove', args=(self.document.pk,),
data={
'cabinets': (cabinet.pk,),
}
)
def test_cabinet_remove_document_view_no_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self._remove_document_from_cabinet(cabinet=cabinet)
self.assertContains(
response, text='Select a valid choice', status_code=200
)
cabinet.refresh_from_db()
self.assertEqual(cabinet.documents.count(), 1)
def test_cabinet_remove_document_view_with_permission(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
self.grant(permission=permission_cabinet_remove_document)
response = self._remove_document_from_cabinet(cabinet=cabinet)
self.assertEqual(response.status_code, 302)
cabinet.refresh_from_db()
self.assertEqual(cabinet.documents.count(), 0)

View File

@@ -0,0 +1,73 @@
from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIDocumentCabinetListView, APICabinetDocumentListView,
APICabinetDocumentView, APICabinetListView, APICabinetView
)
from .views import (
DocumentAddToCabinetView, DocumentCabinetListView,
DocumentRemoveFromCabinetView, CabinetChildAddView, CabinetCreateView,
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
)
urlpatterns = [
url(r'^list/$', CabinetListView.as_view(), name='cabinet_list'),
url(
r'^(?P<pk>\d+)/child/add/$', CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
url(r'^create/$', CabinetCreateView.as_view(), name='cabinet_create'),
url(
r'^(?P<pk>\d+)/edit/$', CabinetEditView.as_view(), name='cabinet_edit'
),
url(
r'^(?P<pk>\d+)/delete/$', CabinetDeleteView.as_view(),
name='cabinet_delete'
),
url(r'^(?P<pk>\d+)/$', CabinetDetailView.as_view(), name='cabinet_view'),
url(
r'^document/(?P<pk>\d+)/cabinet/add/$',
DocumentAddToCabinetView.as_view(), name='cabinet_add_document'
),
url(
r'^document/multiple/cabinet/add/$',
DocumentAddToCabinetView.as_view(),
name='cabinet_add_multiple_documents'
),
url(
r'^document/(?P<pk>\d+)/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(), name='document_cabinet_remove'
),
url(
r'^document/multiple/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove'
),
url(
r'^document/(?P<pk>\d+)/cabinet/list/$',
DocumentCabinetListView.as_view(), name='document_cabinet_list'
),
]
api_urls = [
url(
r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
APICabinetDocumentView.as_view(), name='cabinet-document'
),
url(
r'^cabinets/(?P<pk>[0-9]+)/documents/$',
APICabinetDocumentListView.as_view(), name='cabinet-document-list'
),
url(
r'^cabinets/(?P<pk>[0-9]+)/$', APICabinetView.as_view(),
name='cabinet-detail'
),
url(r'^cabinets/$', APICabinetListView.as_view(), name='cabinet-list'),
url(
r'^documents/(?P<pk>[0-9]+)/cabinets/$',
APIDocumentCabinetListView.as_view(), name='document-cabinet-list'
),
]

View File

@@ -0,0 +1,348 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _, ungettext
from acls.models import AccessControlList
from common.views import (
MultipleObjectFormActionView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView,
TemplateView
)
from documents.permissions import permission_document_view
from documents.models import Document
from .forms import CabinetListForm
from .models import Cabinet
from .permissions import (
permission_cabinet_add_document, permission_cabinet_create,
permission_cabinet_delete, permission_cabinet_edit,
permission_cabinet_view, permission_cabinet_remove_document
)
from .widgets import jstree_data
logger = logging.getLogger(__name__)
class CabinetCreateView(SingleObjectCreateView):
fields = ('label',)
model = Cabinet
view_permission = permission_cabinet_create
def get_extra_context(self):
return {
'title': _('Create cabinet'),
}
class CabinetChildAddView(SingleObjectCreateView):
fields = ('label',)
model = Cabinet
def form_valid(self, form):
"""
If the form is valid, save the associated model.
"""
self.object = form.save(commit=False)
self.object.parent = self.get_object()
self.object.save()
return super(CabinetChildAddView, self).form_valid(form)
def get_object(self, *args, **kwargs):
cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs)
AccessControlList.objects.check_access(
permissions=permission_cabinet_edit, user=self.request.user,
obj=cabinet.get_root()
)
return cabinet
def get_extra_context(self):
return {
'title': _(
'Add new level to: %s'
) % self.get_object().get_full_path(),
}
class CabinetDeleteView(SingleObjectDeleteView):
model = Cabinet
object_permission = permission_cabinet_delete
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Delete the cabinet: %s?') % self.get_object(),
}
class CabinetDetailView(TemplateView):
template_name = 'cabinets/cabinet_details.html'
def get_document_queryset(self):
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.request.user,
queryset=self.get_object().documents.all()
)
return queryset
def get_context_data(self, **kwargs):
data = super(CabinetDetailView, self).get_context_data(**kwargs)
cabinet = self.get_object()
data.update(
{
'jstree_data': '\n'.join(
jstree_data(node=cabinet.get_root(), selected_node=cabinet)
),
'document_list': self.get_document_queryset(),
'hide_links': True,
'object': cabinet,
'title': _('Details of cabinet: %s') % cabinet.get_full_path(),
}
)
return data
def get_object(self):
cabinet = get_object_or_404(Cabinet, pk=self.kwargs['pk'])
if cabinet.is_root_node():
permission_object = cabinet
else:
permission_object = cabinet.get_root()
AccessControlList.objects.check_access(
permissions=permission_cabinet_view, user=self.request.user,
obj=permission_object
)
return cabinet
class CabinetEditView(SingleObjectEditView):
fields = ('label',)
model = Cabinet
object_permission = permission_cabinet_edit
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Edit cabinet: %s') % self.get_object(),
}
class CabinetListView(SingleObjectListView):
model = Cabinet
object_permission = permission_cabinet_view
def get_extra_context(self):
return {
'hide_link': True,
'title': _('Cabinets'),
}
def get_queryset(self):
return Cabinet.objects.root_nodes()
class DocumentCabinetListView(CabinetListView):
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user,
obj=self.document
)
return super(DocumentCabinetListView, self).dispatch(
request, *args, **kwargs
)
def get_extra_context(self):
return {
'hide_link': True,
'object': self.document,
'title': _('Cabinets containing document: %s') % self.document,
}
def get_queryset(self):
return self.document.document_cabinets().all()
class DocumentAddToCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
success_message = _(
'Add to cabinet request performed on %(count)d document'
)
success_message_plural = _(
'Add to cabinet request performed on %(count)d documents'
)
def get_extra_context(self):
queryset = self.get_queryset()
result = {
'submit_label': _('Add'),
'title': ungettext(
'Add document to cabinets',
'Add documents to cabinets',
queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Add document "%s" to cabinets'
) % queryset.first()
}
)
return result
def get_form_extra_kwargs(self):
queryset = self.get_queryset()
result = {
'help_text': _(
'Cabinets to which the selected documents will be added.'
),
'permission': permission_cabinet_add_document,
'user': self.request.user
}
if queryset.count() == 1:
result.update(
{
'queryset': Cabinet.objects.exclude(
pk__in=queryset.first().cabinets.all()
)
}
)
return result
def object_action(self, form, instance):
cabinet_membership = instance.cabinets.all()
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permissions=permission_cabinet_add_document,
user=self.request.user
)
if cabinet in cabinet_membership:
messages.warning(
self.request, _(
'Document: %(document)s is already in '
'cabinet: %(cabinet)s.'
) % {
'document': instance, 'cabinet': cabinet
}
)
else:
cabinet.documents.add(instance)
messages.success(
self.request, _(
'Document: %(document)s added to cabinet: '
'%(cabinet)s successfully.'
) % {
'document': instance, 'cabinet': cabinet
}
)
class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
success_message = _(
'Remove from cabinet request performed on %(count)d document'
)
success_message_plural = _(
'Remove from cabinet request performed on %(count)d documents'
)
def get_extra_context(self):
queryset = self.get_queryset()
result = {
'submit_label': _('Remove'),
'title': ungettext(
'Remove document from cabinets',
'Remove documents from cabinets',
queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Remove document "%s" to cabinets'
) % queryset.first()
}
)
return result
def get_form_extra_kwargs(self):
queryset = self.get_queryset()
result = {
'help_text': _(
'Cabinets from which the selected documents will be removed.'
),
'permission': permission_cabinet_remove_document,
'user': self.request.user
}
if queryset.count() == 1:
result.update(
{
'queryset': queryset.first().cabinets.all()
}
)
return result
def object_action(self, form, instance):
cabinet_membership = instance.cabinets.all()
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permissions=permission_cabinet_remove_document,
user=self.request.user
)
if cabinet not in cabinet_membership:
messages.warning(
self.request, _(
'Document: %(document)s is not in cabinet: '
'%(cabinet)s.'
) % {
'document': instance, 'cabinet': cabinet
}
)
else:
cabinet.documents.remove(instance)
messages.success(
self.request, _(
'Document: %(document)s removed from cabinet: '
'%(cabinet)s.'
) % {
'document': instance, 'cabinet': cabinet
}
)

View File

@@ -0,0 +1,28 @@
from __future__ import unicode_literals
def jstree_data(node, selected_node):
result = []
result.append('{')
result.append('"text": "{}",'.format(node.label))
result.append(
'"state": {{ "opened": true, "selected": {} }},'.format(
'true' if node == selected_node else 'false'
)
)
result.append(
'"data": {{ "href": "{}" }},'.format(node.get_absolute_url())
)
children = node.get_children().order_by('label',)
if children:
result.append('"children" : [')
for child in children:
result.extend(jstree_data(node=child, selected_node=selected_node))
result.append(']')
result.append('},')
return result

View File

@@ -79,6 +79,7 @@ INSTALLED_APPS = (
'smart_settings',
'user_management',
# Mayan EDMS
'cabinets',
'checkouts',
'document_comments',
'document_indexing',