<?php

use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\UID;
use Appwrite\Database\Validator\Key;
use Appwrite\Database\Validator\Structure;
use Appwrite\Database\Validator\Collection;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Exception\Authorization as AuthorizationException;
use Appwrite\Database\Exception\Structure as StructureException;
use Appwrite\Utopia\Response;

App::post('/v1/database/collections')
    ->desc('Create Collection')
    ->groups(['api', 'database'])
    ->label('event', 'database.collections.create')
    ->label('scope', 'collections.write')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_SERVER])
    ->label('sdk.method', 'createCollection')
    ->label('sdk.description', '/docs/references/database/create-collection.md')
    ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_COLLECTION)
    ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
    ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', false, ['projectDB'])
    ->inject('response')
    ->inject('projectDB')
    ->inject('audits')
    ->action(function ($name, $read, $write, $rules, $response, $projectDB, $audits) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $audits */

        $parsedRules = [];

        foreach ($rules as &$rule) {
            $parsedRules[] = \array_merge([
                '$collection' => Database::SYSTEM_COLLECTION_RULES,
                '$permissions' => [
                    'read' => $read,
                    'write' => $write,
                ],
            ], $rule);
        }

        try {
            $data = $projectDB->createDocument([
                '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
                'name' => $name,
                'dateCreated' => \time(),
                'dateUpdated' => \time(),
                'structure' => true,
                '$permissions' => [
                    'read' => $read,
                    'write' => $write,
                ],
                'rules' => $parsedRules,
            ]);
        } catch (AuthorizationException $exception) {
            throw new Exception('Unauthorized permissions', 401);
        } catch (StructureException $exception) {
            throw new Exception('Bad structure. '.$exception->getMessage(), 400);
        } catch (\Exception $exception) {
            throw new Exception('Failed saving document to DB', 500);
        }

        if (false === $data) {
            throw new Exception('Failed saving collection to DB', 500);
        }

        $audits
            ->setParam('event', 'database.collections.create')
            ->setParam('resource', 'database/collection/'.$data->getId())
            ->setParam('data', $data->getArrayCopy())
        ;

        $response
            ->setStatusCode(Response::STATUS_CODE_CREATED)
            ->dynamic($data, Response::MODEL_COLLECTION)
        ;
    });

App::get('/v1/database/collections')
    ->desc('List Collections')
    ->groups(['api', 'database'])
    ->label('scope', 'collections.read')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_SERVER])
    ->label('sdk.method', 'listCollections')
    ->label('sdk.description', '/docs/references/database/list-collections.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_COLLECTION_LIST)
    ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
    ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
    ->param('offset', 0, new Range(0, 40000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
    ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
    ->inject('response')
    ->inject('projectDB')
    ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */

        $results = $projectDB->getCollection([
            'limit' => $limit,
            'offset' => $offset,
            'orderType' => $orderType,
            'search' => $search,
            'filters' => [
                '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS,
            ],
        ]);

        $response->dynamic(new Document([
            'sum' => $projectDB->getSum(),
            'collections' => $results
        ]), Response::MODEL_COLLECTION_LIST);
    });

App::get('/v1/database/collections/:collectionId')
    ->desc('Get Collection')
    ->groups(['api', 'database'])
    ->label('scope', 'collections.read')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_SERVER])
    ->label('sdk.method', 'getCollection')
    ->label('sdk.description', '/docs/references/database/get-collection.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_COLLECTION)
    ->param('collectionId', '', new UID(), 'Collection unique ID.')
    ->inject('response')
    ->inject('projectDB')
    ->action(function ($collectionId, $response, $projectDB) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */

        $collection = $projectDB->getDocument($collectionId, false);

        if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        $response->dynamic($collection, Response::MODEL_COLLECTION);
    });

App::put('/v1/database/collections/:collectionId')
    ->desc('Update Collection')
    ->groups(['api', 'database'])
    ->label('scope', 'collections.write')
    ->label('event', 'database.collections.update')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_SERVER])
    ->label('sdk.method', 'updateCollection')
    ->label('sdk.description', '/docs/references/database/update-collection.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_COLLECTION)
    ->param('collectionId', '', new UID(), 'Collection unique ID.')
    ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
    ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions(/docs/permissions) and get a full list of available permissions.')
    ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true, ['projectDB'])
    ->inject('response')
    ->inject('projectDB')
    ->inject('audits')
    ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $audits) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $audits */

        $collection = $projectDB->getDocument($collectionId, false);

        if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        $parsedRules = [];

        foreach ($rules as &$rule) {
            $parsedRules[] = \array_merge([
                '$collection' => Database::SYSTEM_COLLECTION_RULES,
                '$permissions' => [
                    'read' => $read,
                    'write' => $write,
                ],
            ], $rule);
        }

        try {
            $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [
                'name' => $name,
                'structure' => true,
                'dateUpdated' => \time(),
                '$permissions' => [
                    'read' => $read,
                    'write' => $write,
                ],
                'rules' => $parsedRules,
            ]));
        } catch (AuthorizationException $exception) {
            throw new Exception('Unauthorized permissions', 401);
        } catch (StructureException $exception) {
            throw new Exception('Bad structure. '.$exception->getMessage(), 400);
        } catch (\Exception $exception) {
            throw new Exception('Failed saving document to DB', 500);
        }

        if (false === $collection) {
            throw new Exception('Failed saving collection to DB', 500);
        }

        $audits
            ->setParam('event', 'database.collections.update')
            ->setParam('resource', 'database/collections/'.$collection->getId())
            ->setParam('data', $collection->getArrayCopy())
        ;

        $response->dynamic($collection, Response::MODEL_COLLECTION);
    });

App::delete('/v1/database/collections/:collectionId')
    ->desc('Delete Collection')
    ->groups(['api', 'database'])
    ->label('scope', 'collections.write')
    ->label('event', 'database.collections.delete')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_SERVER])
    ->label('sdk.method', 'deleteCollection')
    ->label('sdk.description', '/docs/references/database/delete-collection.md')
    ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
    ->label('sdk.response.model', Response::MODEL_NONE)
    ->param('collectionId', '', new UID(), 'Collection unique ID.')
    ->inject('response')
    ->inject('projectDB')
    ->inject('events')
    ->inject('audits')
    ->inject('deletes')
    ->action(function ($collectionId, $response, $projectDB, $events, $audits, $deletes) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $events */
        /** @var Appwrite\Event\Event $audits */

        $collection = $projectDB->getDocument($collectionId, false);

        if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        if (!$projectDB->deleteDocument($collectionId)) {
            throw new Exception('Failed to remove collection from DB', 500);
        }

        $deletes
            ->setParam('type', DELETE_TYPE_DOCUMENT)
            ->setParam('document', $collection)
        ;

        $events
            ->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION))
        ;

        $audits
            ->setParam('event', 'database.collections.delete')
            ->setParam('resource', 'database/collections/'.$collection->getId())
            ->setParam('data', $collection->getArrayCopy())
        ;

        $response->noContent();
    });

App::post('/v1/database/collections/:collectionId/documents')
    ->desc('Create Document')
    ->groups(['api', 'database'])
    ->label('event', 'database.documents.create')
    ->label('scope', 'documents.write')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
    ->label('sdk.method', 'createDocument')
    ->label('sdk.description', '/docs/references/database/create-document.md')
    ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_ANY)
    ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
    ->param('data', [], new JSON(), 'Document data as JSON object.')
    ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('parentDocument', '', new UID(), 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true)
    ->param('parentProperty', '', new Key(), 'Parent document property name. Use when you want your new document to be a child of a parent document.', true)
    ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND], true), 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true)
    ->inject('response')
    ->inject('projectDB')
    ->inject('audits')
    ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $audits) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $audits */

        $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array

        if (empty($data)) {
            throw new Exception('Missing payload', 400);
        }

        if (isset($data['$id'])) {
            throw new Exception('$id is not allowed for creating new documents, try update instead', 400);
        }

        $collection = $projectDB->getDocument($collectionId, false);

        if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        $data['$collection'] = $collectionId; // Adding this param to make API easier for developers
        $data['$permissions'] = [
            'read' => $read,
            'write' => $write,
        ];

        // Read parent document + validate not 404 + validate read / write permission like patch method
        // Add payload to parent document property
        if ((!empty($parentDocument)) && (!empty($parentProperty))) {
            $parentDocument = $projectDB->getDocument($parentDocument, false);

            if (empty($parentDocument->getArrayCopy())) { // Check empty
                throw new Exception('No parent document found', 404);
            }

            /*
             * 1. Check child has valid structure,
             * 2. Check user have write permission for parent document
             * 3. Assign parent data (including child) to $data
             * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method)
             */

            $new = new Document($data);

            $structure = new Structure($projectDB);

            if (!$structure->isValid($new)) {
                throw new Exception('Invalid data structure: '.$structure->getDescription(), 400);
            }

            $authorization = new Authorization($parentDocument, 'write');

            if (!$authorization->isValid($new->getPermissions())) {
                throw new Exception('Unauthorized permissions', 401);
            }

            $parentDocument
                ->setAttribute($parentProperty, $data, $parentPropertyType);

            $data = $parentDocument->getArrayCopy();
            $collection = $projectDB->getDocument($parentDocument->getCollection(), false);
        }

        /**
         * Set default collection values
         */
        foreach ($collection->getAttribute('rules') as $key => $rule) {
            $key = $rule['key'] ?? '';
            $default = $rule['default'] ?? null;

            if (!isset($data[$key])) {
                $data[$key] = $default;
            }
        }

        try {
            $data = $projectDB->createDocument($data);
        } catch (AuthorizationException $exception) {
            throw new Exception('Unauthorized permissions', 401);
        } catch (StructureException $exception) {
            throw new Exception('Bad structure. '.$exception->getMessage(), 400);
        } catch (\Exception $exception) {
            throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500);
        }

        $audits
            ->setParam('event', 'database.documents.create')
            ->setParam('resource', 'database/document/'.$data['$id'])
            ->setParam('data', $data->getArrayCopy())
        ;

        $response
            ->setStatusCode(Response::STATUS_CODE_CREATED)
            ->dynamic($data, Response::MODEL_ANY)
        ;
    });

App::get('/v1/database/collections/:collectionId/documents')
    ->desc('List Documents')
    ->groups(['api', 'database'])
    ->label('scope', 'documents.read')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
    ->label('sdk.method', 'listDocuments')
    ->label('sdk.description', '/docs/references/database/list-documents.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST)
    ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
    ->param('filters', [], new ArrayList(new Text(128)), 'Array of filter strings. Each filter is constructed from a key name, comparison operator (=, !=, >, <, <=, >=) and a value. You can also use a dot (.) separator in attribute names to filter by child document attributes. Examples: \'name=John Doe\' or \'category.$id>=5bed2d152c362\'.', true)
    ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response.  Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
    ->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
    ->param('orderField', '', new Text(128), 'Document field that results will be sorted by.', true)
    ->param('orderType', 'ASC', new WhiteList(['DESC', 'ASC'], true), 'Order direction. Possible values are DESC for descending order, or ASC for ascending order.', true)
    ->param('orderCast', 'string', new WhiteList(['int', 'string', 'date', 'time', 'datetime'], true), 'Order field type casting. Possible values are int, string, date, time or datetime. The database will attempt to cast the order field to the value you pass here. The default value is a string.', true)
    ->param('search', '', new Text(256), 'Search query. Enter any free text search. The database will try to find a match against all document attributes and children. Max length: 256 chars.', true)
    ->inject('response')
    ->inject('projectDB')
    ->action(function ($collectionId, $filters, $limit, $offset, $orderField, $orderType, $orderCast, $search, $response, $projectDB) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */

        $collection = $projectDB->getDocument($collectionId, false);

        if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        $list = $projectDB->getCollection([
            'limit' => $limit,
            'offset' => $offset,
            'orderField' => $orderField,
            'orderType' => $orderType,
            'orderCast' => $orderCast,
            'search' => $search,
            'filters' => \array_merge($filters, [
                '$collection='.$collectionId,
            ]),
        ]);

        // if (App::isDevelopment()) {
        //     $collection
        //         ->setAttribute('debug', $projectDB->getDebug())
        //         ->setAttribute('limit', $limit)
        //         ->setAttribute('offset', $offset)
        //         ->setAttribute('orderField', $orderField)
        //         ->setAttribute('orderType', $orderType)
        //         ->setAttribute('orderCast', $orderCast)
        //         ->setAttribute('filters', $filters)
        //     ;
        // }

        $collection
            ->setAttribute('sum', $projectDB->getSum())
            ->setAttribute('documents', $list)
        ;

        $response->dynamic($collection, Response::MODEL_DOCUMENT_LIST);
    });

App::get('/v1/database/collections/:collectionId/documents/:documentId')
    ->desc('Get Document')
    ->groups(['api', 'database'])
    ->label('scope', 'documents.read')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
    ->label('sdk.method', 'getDocument')
    ->label('sdk.description', '/docs/references/database/get-document.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_ANY)
    ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
    ->param('documentId', null, new UID(), 'Document unique ID.')
    ->inject('response')
    ->inject('projectDB')
    ->action(function ($collectionId, $documentId, $response, $projectDB) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */

        $document = $projectDB->getDocument($documentId, false);
        $collection = $projectDB->getDocument($collectionId, false);

        if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty
            throw new Exception('No document found', 404);
        }

        $response->dynamic($document, Response::MODEL_ANY);
    });

App::patch('/v1/database/collections/:collectionId/documents/:documentId')
    ->desc('Update Document')
    ->groups(['api', 'database'])
    ->label('event', 'database.documents.update')
    ->label('scope', 'documents.write')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
    ->label('sdk.method', 'updateDocument')
    ->label('sdk.description', '/docs/references/database/update-document.md')
    ->label('sdk.response.code', Response::STATUS_CODE_OK)
    ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
    ->label('sdk.response.model', Response::MODEL_ANY)
    ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
    ->param('documentId', null, new UID(), 'Document unique ID.')
    ->param('data', [], new JSON(), 'Document data as JSON object.')
    ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
    ->inject('response')
    ->inject('projectDB')
    ->inject('audits')
    ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $audits) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $audits */

        $collection = $projectDB->getDocument($collectionId, false);
        $document = $projectDB->getDocument($documentId, false);

        $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array

        if (!\is_array($data)) {
            throw new Exception('Data param should be a valid JSON object', 400);
        }

        if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty
            throw new Exception('No document found', 404);
        }

        //TODO check merge read write permissions

        if (!empty($read)) { // Overwrite permissions only when passed
            $data['$permissions']['read'] = $read;
        }

        if (!empty($write)) { // Overwrite permissions only when passed
            $data['$permissions']['write'] = $write;
        }

        $data = \array_merge($document->getArrayCopy(), $data);

        $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID
        $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID

        if (empty($data)) {
            throw new Exception('Missing payload', 400);
        }

        try {
            $data = $projectDB->updateDocument($data);
        } catch (AuthorizationException $exception) {
            throw new Exception('Unauthorized permissions', 401);
        } catch (StructureException $exception) {
            throw new Exception('Bad structure. '.$exception->getMessage(), 400);
        } catch (\Exception $exception) {
            throw new Exception('Failed saving document to DB', 500);
        }

        $audits
            ->setParam('event', 'database.documents.update')
            ->setParam('resource', 'database/document/'.$data->getId())
            ->setParam('data', $data->getArrayCopy())
        ;

        $response->dynamic($data, Response::MODEL_ANY);
    });

App::delete('/v1/database/collections/:collectionId/documents/:documentId')
    ->desc('Delete Document')
    ->groups(['api', 'database'])
    ->label('scope', 'documents.write')
    ->label('event', 'database.documents.delete')
    ->label('sdk.namespace', 'database')
    ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
    ->label('sdk.method', 'deleteDocument')
    ->label('sdk.description', '/docs/references/database/delete-document.md')
    ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
    ->label('sdk.response.model', Response::MODEL_NONE)
    ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
    ->param('documentId', null, new UID(), 'Document unique ID.')
    ->inject('response')
    ->inject('projectDB')
    ->inject('events')
    ->inject('audits')
    ->action(function ($collectionId, $documentId, $response, $projectDB, $events, $audits) {
        /** @var Appwrite\Utopia\Response $response */
        /** @var Appwrite\Database\Database $projectDB */
        /** @var Appwrite\Event\Event $events */
        /** @var Appwrite\Event\Event $audits */

        $collection = $projectDB->getDocument($collectionId, false);
        $document = $projectDB->getDocument($documentId, false);

        if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty
            throw new Exception('No document found', 404);
        }

        if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
            throw new Exception('Collection not found', 404);
        }

        try {
            $projectDB->deleteDocument($documentId);
        } catch (AuthorizationException $exception) {
            throw new Exception('Unauthorized permissions', 401);
        } catch (StructureException $exception) {
            throw new Exception('Bad structure. '.$exception->getMessage(), 400);
        } catch (\Exception $exception) {
            throw new Exception('Failed to remove document from DB', 500);
        }

        $events
            ->setParam('payload', $response->output($document, Response::MODEL_ANY))
        ;

        $audits
            ->setParam('event', 'database.documents.delete')
            ->setParam('resource', 'database/document/'.$document->getId())
            ->setParam('data', $document->getArrayCopy()) // Audit document in case of malicious or disastrous action
        ;

        $response->noContent();
    });


if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]

if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]

<?php

use Appwrite\Client;
use Appwrite\Services\Database;

$client = new Client();

$client
    ->setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    ->setProject('5df5acd0d48c2') // Your project ID
    ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
;

$database = new Database($client);

$result = $database->listCollections();


#!/bin/sh

if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
    REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
    REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi

QUEUE='v1-certificates' APP_INCLUDE='/usr/src/code/app/workers/certificates.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php


from appwrite.client import Client
from appwrite.services.database import Database

client = Client()

(client
  .set_endpoint('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint
  .set_project('5df5acd0d48c2') # Your project ID
  .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key
)

database = Database(client)

result = database.get_collection('[COLLECTION_ID]')


const sdk = require('node-appwrite');

// Init SDK
let client = new sdk.Client();

let avatars = new sdk.Avatars(client);

client
    .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    .setProject('5df5acd0d48c2') // Your project ID
    .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
;

let promise = avatars.getFlag('af');

promise.then(function (response) {
    console.log(response);
}, function (error) {
    console.log(error);
});


require 'appwrite'

client = Appwrite::Client.new()

client
    .set_endpoint('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint
    .set_project('5df5acd0d48c2') # Your project ID
    .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key
;

database = Appwrite::Database.new(client);

response = database.get_document(collection_id: '[COLLECTION_ID]', document_id: '[DOCUMENT_ID]');

puts response


keep keep replace keep keep keep keep keep

#!/bin/sh

if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
    REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
    REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi

QUEUE='v1-functions' APP_INCLUDE='/usr/src/code/app/workers/functions.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php


const sdk = require('node-appwrite');

// Init SDK
let client = new sdk.Client();

let users = new sdk.Users(client);

client
    .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    .setProject('5df5acd0d48c2') // Your project ID
    .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
;

let promise = users.list();

promise.then(function (response) {
    console.log(response);
}, function (error) {
    console.log(error);
});


<?php

namespace Tests\E2E\Services\Users;

use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;

class UsersCustomServerTest extends Scope
{
    use UsersBase;
    use ProjectCustom;
    use SideServer;
}


<?php

return [
    'settings.inspire' => '"The art of being wise is the art of knowing what to overlook."', // This is the line printed in the homepage and console 'view-source'
    'settings.locale' => 'fi',
    'settings.direction' => 'ltr',

    // Service - Users
    'account.emails.team' => '%s Tiimi',
    'account.emails.verification.title' => 'Tilin Vahvistus',
    'account.emails.verification.body' => 'fi.email.auth.confirm.tpl',
    'account.emails.recovery.title' => 'Salasanan Nollaus',
    'account.emails.recovery.body' => 'fi.email.auth.recovery.tpl',
    'account.emails.invitation.title' => 'Kutsu %s Tiimiin %s',
    'account.emails.invitation.body' => 'fi.email.auth.invitation.tpl',

    'locale.country.unknown' => 'Unknown',

    'countries' => include 'fi.countries.php',
    'continents' => include 'fi.continents.php',
];


#!/bin/sh

if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
    REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
    REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi

QUEUE='v1-audits' APP_INCLUDE='/usr/src/code/app/workers/audits.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php


<?php

namespace Appwrite\Database\Exception;

class Authorization extends \Exception
{
}


    Halló,

    Hesa teldupost var send til tín tí {{owner}} vildi bjóða tær at vera eitt lið limur hjá {{team}} á {{project}}

    Fylg hettar lenka fyrið at bliva vi {{team}} lið:

{{cta}}

    Om tú ikke er áhugaður, kannst tú ignorera hesa boð

    Takk,

    {{project}} lið


#!/bin/sh

if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
    REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
    REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi

QUEUE='v1-mails' APP_INCLUDE='/usr/src/code/app/workers/mails.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php


import 'package:dart_appwrite/dart_appwrite.dart';

void main() { // Init SDK
  Client client = Client();
  Database database = Database(client);

  client
    .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    .setProject('5df5acd0d48c2') // Your project ID
    .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
  ;

  Future result = database.createCollection(
    name: '[NAME]',
    read: [],
    write: [],
    rules: [],
  );

  result
    .then((response) {
      print(response);
    }).catchError((error) {
      print(error.response);
  });
}


appwrite avatars getImage --url="https://example.com" --width="0" --height="0"


let sdk = new Appwrite();

sdk
    .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    .setProject('5df5acd0d48c2') // Your project ID
;

let promise = sdk.teams.getMemberships('[TEAM_ID]');

promise.then(function (response) {
    console.log(response); // Success
}, function (error) {
    console.log(error); // Failure
});


keep keep keep keep keep replace replace keep keep keep

const scopedSlot = data.scopedSlots && data.scopedSlots.hasOwnProperty(slotName) && data.scopedSlots[slotName]

const regularSlot = computedSlots.hasOwnProperty(slotName) && computedSlots[slotName]

import Vue, { VNode, PropType } from 'vue'
import { getObjectValueByPath } from '../../util/helpers'
import { DataTableHeader } from 'vuetify/types'

export default Vue.extend({
  name: 'row',

  functional: true,

  props: {
    headers: Array as PropType<DataTableHeader[]>,
    hideDefaultHeader: Boolean,
    index: Number,
    item: Object,
    rtl: Boolean,
  },

  render (h, { props, slots, data }): VNode {
    const computedSlots = slots()

    const columns: VNode[] = props.headers.map((header: DataTableHeader) => {
      const classes = {
        'v-data-table__mobile-row': true,
      }

      const children = []
      const value = getObjectValueByPath(props.item, header.value)

      const slotName = header.value
      const scopedSlot = data.scopedSlots && data.scopedSlots[slotName]
      const regularSlot = computedSlots[slotName]

      if (scopedSlot) {
        children.push(scopedSlot({
          item: props.item,
          isMobile: true,
          header,
          index: props.index,
          value,
        }))
      } else if (regularSlot) {
        children.push(regularSlot)
      } else {
        children.push(value == null ? value : String(value))
      }

      const mobileRowChildren = [
        h('div', {
          staticClass: 'v-data-table__mobile-row__cell',
        }, children),
      ]

      if (header.value !== 'dataTableSelect' && !props.hideDefaultHeader) {
        mobileRowChildren.unshift(
          h('div', {
            staticClass: 'v-data-table__mobile-row__header',
          }, [header.text])
        )
      }

      return h('td', { class: classes }, mobileRowChildren)
    })

    return h('tr', { ...data, staticClass: 'v-data-table__mobile-table-row' }, columns)
  },
})


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`times.ts should parse timestamp 1`] = `
Object {
  "date": "2019-02-08",
  "day": 8,
  "future": false,
  "hasDay": true,
  "hasTime": false,
  "hour": 0,
  "minute": 0,
  "month": 2,
  "past": false,
  "present": false,
  "time": "",
  "weekday": 5,
  "year": 2019,
}
`;


// Components
import VOverflowBtn from '../VOverflowBtn'

// Utilities
import {
  mount,
  Wrapper,
} from '@vue/test-utils'
import { ExtractVue } from '../../../util/mixins'

describe('VOverflowBtn.js', () => {
  type Instance = ExtractVue<typeof VOverflowBtn>
  let mountFunction: (options?: object) => Wrapper<Instance>

  beforeEach(() => {
    document.body.setAttribute('data-app', 'true')

    mountFunction = (options = {}) => {
      return mount(VOverflowBtn, {
        ...options,
        // https://github.com/vuejs/vue-test-utils/issues/1130
        sync: false,
        mocks: {
          $vuetify: {
            lang: {
              t: (val: string) => val,
            },
            theme: {
              dark: false,
            },
          },
        },
      })
    }
  })

  const warning = 'items must contain both a text and callback property'

  it.skip('segmented - should warn when item has no callback', async () => {
    const items = [
      { text: 'Hello' },
      { text: 'Hello' },
    ]

    const wrapper = mountFunction({
      propsData: {
        segmented: true,
        items,
      },
    })

    await wrapper.vm.$nextTick()

    // The error only happens
    // when generating the button
    // which only happens when
    // we have a matching model
    wrapper.setProps({
      items: [items[1]],
      value: 'Hello',
    })

    await wrapper.vm.$nextTick()

    expect(warning).toHaveBeenTipped()
  })

  it('should use default autocomplete selections', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: ['foo'],
        multiple: true,
        value: ['foo'],
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({
      items: [{ text: 'foo', value: 'foo', callback: () => { } }],
      multiple: false,
      segmented: true,
      value: 'foo',
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should invoke item callback', () => {
    const callback = jest.fn()
    const wrapper = mountFunction({
      propsData: {
        items: [{
          text: 'foo',
          value: 'bar',
          callback,
        }],
        segmented: true,
        value: 'bar',
      },
    })

    const btn = wrapper.find('.v-btn')

    btn.trigger('click')

    expect(callback).toHaveBeenCalled()
  })
})


import './VGrid.sass'

import Vue, { PropOptions } from 'vue'
import mergeData from '../../util/mergeData'
import { upperFirst } from '../../util/helpers'

// no xs
const breakpoints = ['sm', 'md', 'lg', 'xl']

const ALIGNMENT = ['start', 'end', 'center']

function makeProps (prefix: string, def: () => PropOptions) {
  return breakpoints.reduce((props, val) => {
    props[prefix + upperFirst(val)] = def()
    return props
  }, {} as Dictionary<PropOptions>)
}

const alignValidator = (str: any) => [...ALIGNMENT, 'baseline', 'stretch'].includes(str)
const alignProps = makeProps('align', () => ({
  type: String,
  default: null,
  validator: alignValidator,
}))

const justifyValidator = (str: any) => [...ALIGNMENT, 'space-between', 'space-around'].includes(str)
const justifyProps = makeProps('justify', () => ({
  type: String,
  default: null,
  validator: justifyValidator,
}))

const alignContentValidator = (str: any) => [...ALIGNMENT, 'space-between', 'space-around', 'stretch'].includes(str)
const alignContentProps = makeProps('alignContent', () => ({
  type: String,
  default: null,
  validator: alignContentValidator,
}))

const propMap = {
  align: Object.keys(alignProps),
  justify: Object.keys(justifyProps),
  alignContent: Object.keys(alignContentProps),
}

const classMap = {
  align: 'align',
  justify: 'justify',
  alignContent: 'align-content',
}

function breakpointClass (type: keyof typeof propMap, prop: string, val: string) {
  let className = classMap[type]
  if (val == null) {
    return undefined
  }
  if (prop) {
    // alignSm -> Sm
    const breakpoint = prop.replace(type, '')
    className += `-${breakpoint}`
  }
  // .align-items-sm-center
  className += `-${val}`
  return className.toLowerCase()
}

const cache = new Map<string, any[]>()

export default Vue.extend({
  name: 'v-row',
  functional: true,
  props: {
    tag: {
      type: String,
      default: 'div',
    },
    dense: Boolean,
    noGutters: Boolean,
    align: {
      type: String,
      default: null,
      validator: alignValidator,
    },
    ...alignProps,
    justify: {
      type: String,
      default: null,
      validator: justifyValidator,
    },
    ...justifyProps,
    alignContent: {
      type: String,
      default: null,
      validator: alignContentValidator,
    },
    ...alignContentProps,
  },
  render (h, { props, data, children }) {
    // Super-fast memoization based on props, 5x faster than JSON.stringify
    let cacheKey = ''
    for (const prop in props) {
      cacheKey += String((props as any)[prop])
    }
    let classList = cache.get(cacheKey)

    if (!classList) {
      classList = []
      // Loop through `align`, `justify`, `alignContent` breakpoint props
      let type: keyof typeof propMap
      for (type in propMap) {
        propMap[type].forEach(prop => {
          const value: string = (props as any)[prop]
          const className = breakpointClass(type, prop, value)
          if (className) classList!.push(className)
        })
      }

      classList.push({
        'no-gutters': props.noGutters,
        'row--dense': props.dense,
        [`align-${props.align}`]: props.align,
        [`justify-${props.justify}`]: props.justify,
        [`align-content-${props.alignContent}`]: props.alignContent,
      })

      cache.set(cacheKey, classList)
    }

    return h(
      props.tag,
      mergeData(data, {
        staticClass: 'row',
        class: classList,
      }),
      children
    )
  },
})


keep keep keep keep replace replace keep keep keep keep keep

import Vue from 'vue'
import { VNode, VNodeDirective } from 'vue/types'
import { VuetifyIcon } from 'vuetify/types/services/icons'
import { DataTableCompareFunction, SelectItemKey, ItemGroup } from 'vuetify/types'

export function createSimpleFunctional (
  c: string,
  el = 'div',
  name?: string
) {
  return Vue.extend({
    name: name || c.replace(/__/g, '-'),

    functional: true,

    props: {
      tag: {
        type: String,
        default: el,
      },
    },

    render (h, { data, props, children }): VNode {
      data.staticClass = (`${c} ${data.staticClass || ''}`).trim()

      return h(props.tag, data, children)
    },
  })
}

export type BindingConfig = Pick<VNodeDirective, 'arg' | 'modifiers' | 'value'>
export function directiveConfig (binding: BindingConfig, defaults = {}): VNodeDirective {
  return {
    ...defaults,
    ...binding.modifiers,
    value: binding.arg,
    ...(binding.value || {}),
  }
}

export function addOnceEventListener (
  el: EventTarget,
  eventName: string,
  cb: (event: Event) => void,
  options: boolean | AddEventListenerOptions = false
): void {
  const once = (event: Event) => {
    cb(event)
    el.removeEventListener(eventName, once, options)
  }

  el.addEventListener(eventName, once, options)
}

let passiveSupported = false
try {
  if (typeof window !== 'undefined') {
    const testListenerOpts = Object.defineProperty({}, 'passive', {
      get: () => {
        passiveSupported = true
      },
    })

    window.addEventListener('testListener', testListenerOpts, testListenerOpts)
    window.removeEventListener('testListener', testListenerOpts, testListenerOpts)
  }
} catch (e) { console.warn(e) } /* eslint-disable-line no-console */
export { passiveSupported }

export function addPassiveEventListener (
  el: EventTarget,
  event: string,
  cb: EventHandlerNonNull | (() => void),
  options: {}
): void {
  el.addEventListener(event, cb, passiveSupported ? options : false)
}

export function getNestedValue (obj: any, path: (string | number)[], fallback?: any): any {
  const last = path.length - 1

  if (last < 0) return obj === undefined ? fallback : obj

  for (let i = 0; i < last; i++) {
    if (obj == null) {
      return fallback
    }
    obj = obj[path[i]]
  }

  if (obj == null) return fallback

  return obj[path[last]] === undefined ? fallback : obj[path[last]]
}

export function deepEqual (a: any, b: any): boolean {
  if (a === b) return true

  if (
    a instanceof Date &&
    b instanceof Date &&
    a.getTime() !== b.getTime()
  ) {
    // If the values are Date, compare them as timestamps
    return false
  }

  if (a !== Object(a) || b !== Object(b)) {
    // If the values aren't objects, they were already checked for equality
    return false
  }

  const props = Object.keys(a)

  if (props.length !== Object.keys(b).length) {
    // Different number of props, don't bother to check
    return false
  }

  return props.every(p => deepEqual(a[p], b[p]))
}

export function getObjectValueByPath (obj: any, path: string, fallback?: any): any {
  // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
  if (obj == null || !path || typeof path !== 'string') return fallback
  if (obj[path] !== undefined) return obj[path]
  path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
  path = path.replace(/^\./, '') // strip a leading dot
  return getNestedValue(obj, path.split('.'), fallback)
}

export function getPropertyFromItem (
  item: object,
  property: SelectItemKey,
  fallback?: any
): any {
  if (property == null) return item === undefined ? fallback : item

  if (item !== Object(item)) return fallback === undefined ? item : fallback

  if (typeof property === 'string') return getObjectValueByPath(item, property, fallback)

  if (Array.isArray(property)) return getNestedValue(item, property, fallback)

  if (typeof property !== 'function') return fallback

  const value = property(item, fallback)

  return typeof value === 'undefined' ? fallback : value
}

export function createRange (length: number): number[] {
  return Array.from({ length }, (v, k) => k)
}

export function getZIndex (el?: Element | null): number {
  if (!el || el.nodeType !== Node.ELEMENT_NODE) return 0

  const index = +window.getComputedStyle(el).getPropertyValue('z-index')

  if (!index) return getZIndex(el.parentNode as Element)
  return index
}

const tagsToReplace = {
  '&': '&',
  '<': '<',
  '>': '>',
} as any

export function escapeHTML (str: string): string {
  return str.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag)
}

export function filterObjectOnKeys<T, K extends keyof T> (obj: T, keys: K[]): { [N in K]: T[N] } {
  const filtered = {} as { [N in K]: T[N] }

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    if (typeof obj[key] !== 'undefined') {
      filtered[key] = obj[key]
    }
  }

  return filtered
}

export function convertToUnit (str: string | number | null | undefined, unit = 'px'): string | undefined {
  if (str == null || str === '') {
    return undefined
  } else if (isNaN(+str!)) {
    return String(str)
  } else {
    return `${Number(str)}${unit}`
  }
}

export function kebabCase (str: string): string {
  return (str || '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

export function isObject (obj: any): obj is object {
  return obj !== null && typeof obj === 'object'
}

// KeyboardEvent.keyCode aliases
export const keyCodes = Object.freeze({
  enter: 13,
  tab: 9,
  delete: 46,
  esc: 27,
  space: 32,
  up: 38,
  down: 40,
  left: 37,
  right: 39,
  end: 35,
  home: 36,
  del: 46,
  backspace: 8,
  insert: 45,
  pageup: 33,
  pagedown: 34,
  shift: 16,
})

/**
 * This remaps internal names like '$cancel' or '$vuetify.icons.cancel'
 * to the current name or component for that icon.
 */
export function remapInternalIcon (vm: Vue, iconName: string): VuetifyIcon {
  // Look for custom component in the configuration
  const component = vm.$vuetify.icons.component

  // Look for overrides
  if (iconName.startsWith('$')) {
    // Get the target icon name
    const iconPath = `$vuetify.icons.values.${iconName.split('$').pop()!.split('.').pop()}`

    // Now look up icon indirection name,
    // e.g. '$vuetify.icons.values.cancel'
    const override = getObjectValueByPath(vm, iconPath, iconName)

    if (typeof override === 'string') iconName = override
    else return override
  }

  if (component == null) {
    return iconName
  }

  return {
    component,
    props: {
      icon: iconName,
    },
  }
}

export function keys<O> (o: O) {
  return Object.keys(o) as (keyof O)[]
}

/**
 * Camelize a hyphen-delimited string.
 */
const camelizeRE = /-(\w)/g
export const camelize = (str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}

/**
 * Returns the set difference of B and A, i.e. the set of elements in B but not in A
 */
export function arrayDiff (a: any[], b: any[]): any[] {
  const diff: any[] = []
  for (let i = 0; i < b.length; i++) {
    if (a.indexOf(b[i]) < 0) diff.push(b[i])
  }
  return diff
}

/**
 * Makes the first character of a string uppercase
 */
export function upperFirst (str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

export function groupItems<T extends any = any> (
  items: T[],
  groupBy: string[],
  groupDesc: boolean[]
): ItemGroup<T>[] {
  const key = groupBy[0]
  const groups: ItemGroup<T>[] = []
  let current
  for (let i = 0; i < items.length; i++) {
    const item = items[i]
    const val = getObjectValueByPath(item, key, null)
    if (current !== val) {
      current = val
      groups.push({
        name: val ?? '',
        items: [],
      })
    }
    groups[groups.length - 1].items.push(item)
  }
  return groups
}

export function wrapInArray<T> (v: T | T[] | null | undefined): T[] { return v != null ? Array.isArray(v) ? v : [v] : [] }

export function sortItems<T extends any = any> (
  items: T[],
  sortBy: string[],
  sortDesc: boolean[],
  locale: string,
  customSorters?: Record<string, DataTableCompareFunction<T>>
): T[] {
  if (sortBy === null || !sortBy.length) return items
  const stringCollator = new Intl.Collator(locale, { sensitivity: 'accent', usage: 'sort' })

  return items.sort((a, b) => {
    for (let i = 0; i < sortBy.length; i++) {
      const sortKey = sortBy[i]

      let sortA = getObjectValueByPath(a, sortKey)
      let sortB = getObjectValueByPath(b, sortKey)

      if (sortDesc[i]) {
        [sortA, sortB] = [sortB, sortA]
      }

      if (customSorters && customSorters[sortKey]) {
        const customResult = customSorters[sortKey](sortA, sortB)

        if (!customResult) continue

        return customResult
      }

      // Check if both cannot be evaluated
      if (sortA === null && sortB === null) {
        continue
      }

      // Dates should be compared numerically
      if (sortA instanceof Date && sortB instanceof Date) {
        return sortA.getTime() - sortB.getTime()
      }

      [sortA, sortB] = [sortA, sortB].map(s => (s || '').toString().toLocaleLowerCase())

      if (sortA !== sortB) {
        if (!isNaN(sortA) && !isNaN(sortB)) return Number(sortA) - Number(sortB)
        return stringCollator.compare(sortA, sortB)
      }
    }

    return 0
  })
}

export function defaultFilter (value: any, search: string | null, item: any) {
  return value != null &&
    search != null &&
    typeof value !== 'boolean' &&
    value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1
}

export function searchItems<T extends any = any> (items: T[], search: string): T[] {
  if (!search) return items
  search = search.toString().toLowerCase()
  if (search.trim() === '') return items

  return items.filter((item: any) => Object.keys(item).some(key => defaultFilter(getObjectValueByPath(item, key), search, item)))
}

/**
 * Returns:
 *  - 'normal' for old style slots - `<template slot="default">`
 *  - 'scoped' for old style scoped slots (`<template slot="default" slot-scope="data">`) or bound v-slot (`#default="data"`)
 *  - 'v-slot' for unbound v-slot (`#default`) - only if the third param is true, otherwise counts as scoped
 */
export function getSlotType<T extends boolean = false> (vm: Vue, name: string, split?: T): (T extends true ? 'v-slot' : never) | 'normal' | 'scoped' | void {
  if (vm.$slots[name] && vm.$scopedSlots[name] && (vm.$scopedSlots[name] as any).name) {
    return split ? 'v-slot' as any : 'scoped'
  }
  if (vm.$slots[name]) return 'normal'
  if (vm.$scopedSlots[name]) return 'scoped'
}

export function debounce (fn: Function, delay: number) {
  let timeoutId = 0 as any
  return (...args: any[]) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }
}

export function throttle<T extends (...args: any[]) => any> (fn: T, limit: number) {
  let throttling = false
  return (...args: Parameters<T>): void | ReturnType<T> => {
    if (!throttling) {
      throttling = true
      setTimeout(() => throttling = false, limit)
      return fn(...args)
    }
  }
}

export function getPrefixedScopedSlots (prefix: string, scopedSlots: any) {
  return Object.keys(scopedSlots).filter(k => k.startsWith(prefix)).reduce((obj: any, k: string) => {
    obj[k.replace(prefix, '')] = scopedSlots[k]
    return obj
  }, {})
}

export function getSlot (vm: Vue, name = 'default', data?: object | (() => object), optional = false) {
  if (vm.$scopedSlots[name]) {
    return vm.$scopedSlots[name]!(data instanceof Function ? data() : data)
  } else if (vm.$slots[name] && (!data || optional)) {
    return vm.$slots[name]
  }
  return undefined
}

export function clamp (value: number, min = 0, max = 1) {
  return Math.max(min, Math.min(max, value))
}

export function padEnd (str: string, length: number, char = '0') {
  return str + char.repeat(Math.max(0, length - str.length))
}

export function chunk (str: string, size = 1) {
  const chunked: string[] = []
  let index = 0
  while (index < str.length) {
    chunked.push(str.substr(index, size))
    index += size
  }
  return chunked
}

export function humanReadableFileSize (bytes: number, binary = false): string {
  const base = binary ? 1024 : 1000
  if (bytes < base) {
    return `${bytes} B`
  }

  const prefix = binary ? ['Ki', 'Mi', 'Gi'] : ['k', 'M', 'G']
  let unit = -1
  while (Math.abs(bytes) >= base && unit < prefix.length - 1) {
    bytes /= base
    ++unit
  }
  return `${bytes.toFixed(1)} ${prefix[unit]}B`
}

export function camelizeObjectKeys (obj: Record<string, any> | null | undefined) {
  if (!obj) return {}

  return Object.keys(obj).reduce((o: any, key: string) => {
    o[camelize(key)] = obj[key]
    return o
  }, {})
}

export function mergeDeep (
  source: Dictionary<any> = {},
  target: Dictionary<any> = {}
) {
  for (const key in target) {
    const sourceProperty = source[key]
    const targetProperty = target[key]

    // Only continue deep merging if
    // both properties are objects
    if (
      isObject(sourceProperty) &&
      isObject(targetProperty)
    ) {
      source[key] = mergeDeep(sourceProperty, targetProperty)

      continue
    }

    source[key] = targetProperty
  }

  return source
}

export function fillArray<T> (length: number, obj: T) {
  return Array(length).fill(obj)
}

/**  Polyfill for Event.prototype.composedPath */
export function composedPath (e: Event): EventTarget[] {
  if (e.composedPath) return e.composedPath()

  const path = []
  let el = e.target as Element

  while (el) {
    path.push(el)

    if (el.tagName === 'HTML') {
      path.push(document)
      path.push(window)

      return path
    }

    el = el.parentElement!
  }
  return path
}


<template>
  <v-card
    class="mx-auto"
    max-width="500"
  >
    <v-toolbar
      color="teal"
      dark
    >
      <v-app-bar-nav-icon></v-app-bar-nav-icon>

      <v-toolbar-title>Topics</v-toolbar-title>

      <v-spacer></v-spacer>

      <v-btn icon>
        <v-icon>mdi-dots-vertical</v-icon>
      </v-btn>
    </v-toolbar>

    <v-list>
      <v-list-group
        v-for="item in items"
        :key="item.title"
        v-model="item.active"
        :prepend-icon="item.action"
        no-action
      >
        <template v-slot:activator>
          <v-list-item-content>
            <v-list-item-title v-text="item.title"></v-list-item-title>
          </v-list-item-content>
        </template>

        <v-list-item
          v-for="child in item.items"
          :key="child.title"
        >
          <v-list-item-content>
            <v-list-item-title v-text="child.title"></v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list-group>
    </v-list>
  </v-card>
</template>

<script>
  export default {
    data: () => ({
      items: [
        {
          action: 'mdi-ticket',
          items: [{ title: 'List Item' }],
          title: 'Attractions',
        },
        {
          action: 'mdi-silverware-fork-knife',
          active: true,
          items: [
            { title: 'Breakfast & brunch' },
            { title: 'New American' },
            { title: 'Sushi' },
          ],
          title: 'Dining',
        },
        {
          action: 'mdi-school',
          items: [{ title: 'List Item' }],
          title: 'Education',
        },
        {
          action: 'mdi-run',
          items: [{ title: 'List Item' }],
          title: 'Family',
        },
        {
          action: 'mdi-bottle-tonic-plus',
          items: [{ title: 'List Item' }],
          title: 'Health',
        },
        {
          action: 'mdi-content-cut',
          items: [{ title: 'List Item' }],
          title: 'Office',
        },
        {
          action: 'mdi-tag',
          items: [{ title: 'List Item' }],
          title: 'Promotions',
        },
      ],
    }),
  }
</script>


<template>
  <div>
    <v-alert
      color="#2A3B4D"
      dark
      icon="mdi-firework"
      dense
    >
      Suspendisse enim turpis, dictum sed, iaculis a, condimentum nec, nisi. Vivamus quis mi. Quisque ut nisi. Maecenas malesuada.
    </v-alert>
    <v-alert
      color="#C51162"
      dark
      icon="mdi-material-design"
      border="right"
    >
      Phasellus blandit leo ut odio. Morbi mattis ullamcorper velit. Donec orci lectus, aliquam ut, faucibus non, euismod id, nulla. In ut quam vitae odio lacinia tincidunt.
    </v-alert>
    <v-alert
      color="primary"
      dark
      icon="mdi-vuetify"
      border="left"
      prominent
    >
      Praesent congue erat at massa. Nullam vel sem. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Proin viverra, ligula sit amet ultrices semper, ligula arcu tristique sapien, a accumsan nisi mauris ac eros. Curabitur at lacus ac velit ornare lobortis.
    </v-alert>
  </div>
</template>


<template>
  <v-card
    outlined
    class="pa-4"
  >
    <app-md>
      {{ migration || ' ' }}
    </app-md>
  </v-card>
</template>

<script>
  // Utilities
  import { getBranch } from '@/util/helpers'

  export default {
    name: 'GettingStartedMigrations',

    data: () => ({
      branch: undefined,
      migration: null,
    }),

    mounted () {
      this.branch = getBranch()

      fetch(`https://api.github.com/repos/vuetifyjs/vuetify/contents/MIGRATION.md?ref=${this.branch}`, {
        method: 'get',
        headers: { 'Content-Type': 'application/json' },
      })
        .then(res => res.json())
        .then(res => {
          this.migration = res.content ? Buffer.from(res.content, 'base64').toString() : ' '
        })
        .catch(err => console.log(err))
    },
  }
</script>

<style lang="sass" scoped>
  ::v-deep pre
    background: #2d2d2d !important
    border-radius: 4px
    margin: 16px 0
    padding: 16px

    code
      background: transparent
      background-color: unset !important
      box-shadow: none !important
      color: #ccc !important
      font-family: "Inconsolata", monospace
      font-weight: 300
      font-size: 15px
      line-height: 1.55

  ::v-deep .migration-markdown
    h1, h2, h3, h4, h5, p, pre, ul
      margin-bottom: 16px !important
</style>


if (header.value === 'data-table-select' && !this.singleSelect) {

return this.$createElement('th', data, [this.genSelectAll()])

? this.$scopedSlots[header.value]!({ header })

keep keep keep keep keep replace keep keep keep keep


---
meta:
  title: List component
  description: The list component is a continuous group of text, images and icons that may contain primary or supplemental actions.
  keywords: lists, vuetify list component, vue list component
related:
  - /components/item-groups/
  - /components/list-item-groups/
  - /components/subheaders/
---

# Lists

The `v-list` component is used to display information. It can contain an avatar, content, actions, subheaders and much more. Lists present content in a way that makes it easy to identify a specific item in a collection. They provide a consistent styling for organizing groups of text and images.

<entry-ad />

## Usage

Lists come in three main variations. **single-line** (default), **two-line** and **three-line**. The line declaration specifies the minimum height of the item and can also be controlled from `v-list` with the same prop.

<example file="v-list/usage" />

## API

- [v-list](/api/v-list)
- [v-list-group](/api/v-list-group)
- [v-list-item](/api/v-list-item)
- [v-list-item-action](/api/v-list-item-action)
- [v-list-item-action-text](/api/v-list-item-action-text)
- [v-list-item-avatar](/api/v-list-item-avatar)
- [v-list-item-content](/api/v-list-item-content)
- [v-list-item-group](/api/v-list-item-group)
- [v-list-item-icon](/api/v-list-item-icon)
- [v-list-item-subtitle](/api/v-list-item-subtitle)
- [v-list-item-title](/api/v-list-item-title)

<inline-api page="components/lists" />

<!-- ## Sub-components

### v-list-item

v-list-item description

### v-list-item-action

v-list-item-action description

### v-list-item-action-text

v-list-item-action-text description

### v-list-item-avatar

v-list-item-avatar description

### v-list-item-content

v-list-item-content description

### v-list-item-subtitle

v-list-item-subtitle description

### v-list-item-title

v-list-item-title description -->

## Caveats

<alert type="info">

  If you are looking for stateful list items, please check out [v-list-item-group](/components/list-item-groups).

</alert>

## Examples

### Props

#### Dense

`v-list` can be lowered with **dense** property.

<example file="v-list/prop-dense" />

#### Disabled

You cannot interact with disabled `v-list`.

<promoted-ad slug="vuetify-lux-admin-pro" />

<example file="v-list/prop-disabled" />

#### Flat

Items don't change when selected in `v-list` with **flat** property.

<example file="v-list/prop-flat" />

#### Nav

Lists can receive an alternative **nav** styling that reduces the width `v-list-item` takes up as well as adding a border radius.

<example file="v-list/prop-nav" />

#### Rounded

You can make `v-list` items rounded.

<example file="v-list/prop-rounded" />

#### Shaped

Shaped lists have rounded borders on one side of the `v-list-item`.

<example file="v-list/prop-shaped" />

#### Sub group

Using the `v-list-group` component you can create up to **2** levels in depth using the **sub-group** prop.

<example file="v-list/prop-sub-group" />

#### Three line

For three line lists, the subtitle will clamp vertically at 2 lines and then ellipsis. This feature uses [line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp) and is not supported in all browsers.

<example file="v-list/prop-three-line" />

#### Two lines and subheader

Lists can contain subheaders, dividers, and can contain 1 or more lines. The subtitle will overflow with ellipsis if it extends past one line.

<example file="v-list/prop-two-line-and-subheader" />

### Slots

#### Expansion Lists

A list can contain a group of items which will display on click utilizing `v-list-group`'s `activator` slot. Expansion lists are also used within the **[v-navigation-drawer](/components/navigation-drawers)** component.

<example file="v-list/slot-expansion-lists" />

### Misc

#### Action and item groups

A **three-line** list with actions. Utilizing **[v-list-item-group](/components/list-item-groups)**, easily connect actions to your tiles.

<example file="v-list/misc-action-and-item-groups" />

#### Action stack

A list can contain a stack within an action. This is useful when you need to display meta text next to your action item.

<example file="v-list/misc-action-stack" />

#### Card list

A list can be combined with a card.

<example file="v-list/misc-card-list" />

#### Simple avatar list

A simple list utilizing `v-list-item-icon`, `v-list-item-title` and `v-list-item-avatar`.

<example file="v-list/misc-simple-avatar-list" />

#### Single line list

Here we combine **v-list-item-avatar** and **v-list-item-icon** in a single-line list.

<example file="v-list/misc-single-line-list" />

#### Subheadings and dividers

Lists can contain multiple subheaders and dividers.

<example file="v-list/misc-subheadings-and-dividers" />

<backmatter />


const { VInput } = require('../helpers/variables')

VInput.slots = VInput.slots.filter(slot => !['append', 'prepend'].includes(slot.name))

module.exports = {
  'v-radio': {
    ...VInput,
    events: [
      ...VInput.events,
      {
        name: 'click',
        value: 'MouseEvent',
      },
    ],
  },
}


// Styles
import './VAutocomplete.sass'

// Extensions
import VSelect, { defaultMenuProps as VSelectMenuProps } from '../VSelect/VSelect'
import VTextField from '../VTextField/VTextField'

// Utilities
import mergeData from '../../util/mergeData'
import {
  getObjectValueByPath,
  getPropertyFromItem,
  keyCodes,
} from '../../util/helpers'

// Types
import { PropType, VNode } from 'vue'
import { PropValidator } from 'vue/types/options'

const defaultMenuProps = {
  ...VSelectMenuProps,
  offsetY: true,
  offsetOverflow: true,
  transition: false,
}

/* @vue/component */
export default VSelect.extend({
  name: 'v-autocomplete',

  props: {
    allowOverflow: {
      type: Boolean,
      default: true,
    },
    autoSelectFirst: {
      type: Boolean,
      default: false,
    },
    filter: {
      type: Function,
      default: (item: any, queryText: string, itemText: string) => {
        return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
      },
    } as PropValidator<(item: any, queryText: string, itemText: string) => boolean>,
    hideNoData: Boolean,
    menuProps: {
      type: VSelect.options.props.menuProps.type,
      default: () => defaultMenuProps,
    },
    noFilter: Boolean,
    searchInput: {
      type: String as PropType<string | null>,
    },
  },

  data () {
    return {
      lazySearch: this.searchInput,
    }
  },

  computed: {
    classes (): object {
      return {
        ...VSelect.options.computed.classes.call(this),
        'v-autocomplete': true,
        'v-autocomplete--is-selecting-index': this.selectedIndex > -1,
      }
    },
    computedItems (): object[] {
      return this.filteredItems
    },
    selectedValues (): object[] {
      return this.selectedItems.map(item => this.getValue(item))
    },
    hasDisplayedItems (): boolean {
      return this.hideSelected
        ? this.filteredItems.some(item => !this.hasItem(item))
        : this.filteredItems.length > 0
    },
    currentRange (): number {
      if (this.selectedItem == null) return 0

      return String(this.getText(this.selectedItem)).length
    },
    filteredItems (): object[] {
      if (!this.isSearching || this.noFilter || this.internalSearch == null) return this.allItems

      return this.allItems.filter(item => {
        const value = getPropertyFromItem(item, this.itemText)
        const text = value != null ? String(value) : ''

        return this.filter(item, String(this.internalSearch), text)
      })
    },
    internalSearch: {
      get (): string | null {
        return this.lazySearch
      },
      set (val: any) { // TODO: this should be `string | null` but it breaks lots of other types
        // emit update event only when the new
        // search value is different from previous
        if (this.lazySearch !== val) {
          this.lazySearch = val
          this.$emit('update:search-input', val)
        }
      },
    },
    isAnyValueAllowed (): boolean {
      return false
    },
    isDirty (): boolean {
      return this.searchIsDirty || this.selectedItems.length > 0
    },
    isSearching (): boolean {
      return (
        this.multiple &&
        this.searchIsDirty
      ) || (
        this.searchIsDirty &&
        this.internalSearch !== this.getText(this.selectedItem)
      )
    },
    menuCanShow (): boolean {
      if (!this.isFocused) return false

      return this.hasDisplayedItems || !this.hideNoData
    },
    $_menuProps (): object {
      const props = VSelect.options.computed.$_menuProps.call(this);
      (props as any).contentClass = `v-autocomplete__content ${(props as any).contentClass || ''}`.trim()
      return {
        ...defaultMenuProps,
        ...props,
      }
    },
    searchIsDirty (): boolean {
      return this.internalSearch != null &&
        this.internalSearch !== ''
    },
    selectedItem (): any {
      if (this.multiple) return null

      return this.selectedItems.find(i => {
        return this.valueComparator(this.getValue(i), this.getValue(this.internalValue))
      })
    },
    listData () {
      const data = VSelect.options.computed.listData.call(this) as any

      data.props = {
        ...data.props,
        items: this.virtualizedItems,
        noFilter: (
          this.noFilter ||
          !this.isSearching ||
          !this.filteredItems.length
        ),
        searchInput: this.internalSearch,
      }

      return data
    },
  },

  watch: {
    filteredItems: 'onFilteredItemsChanged',
    internalValue: 'setSearch',
    isFocused (val) {
      if (val) {
        document.addEventListener('copy', this.onCopy)
        this.$refs.input && this.$refs.input.select()
      } else {
        document.removeEventListener('copy', this.onCopy)
        this.blur()
        this.updateSelf()
      }
    },
    isMenuActive (val) {
      if (val || !this.hasSlot) return

      this.lazySearch = null
    },
    items (val, oldVal) {
      // If we are focused, the menu
      // is not active, hide no data is enabled,
      // and items change
      // User is probably async loading
      // items, try to activate the menu
      if (
        !(oldVal && oldVal.length) &&
        this.hideNoData &&
        this.isFocused &&
        !this.isMenuActive &&
        val.length
      ) this.activateMenu()
    },
    searchInput (val: string) {
      this.lazySearch = val
    },
    internalSearch: 'onInternalSearchChanged',
    itemText: 'updateSelf',
  },

  created () {
    this.setSearch()
  },

  destroyed () {
    document.removeEventListener('copy', this.onCopy)
  },

  methods: {
    onFilteredItemsChanged (val: never[], oldVal: never[]) {
      // TODO: How is the watcher triggered
      // for duplicate items? no idea
      if (val === oldVal) return

      if (!this.autoSelectFirst) {
        const preSelectedItem = oldVal[this.$refs.menu.listIndex]

        if (preSelectedItem) {
          this.setMenuIndex(val.findIndex(i => i === preSelectedItem))
        } else {
          this.setMenuIndex(-1)
        }
        this.$emit('update:list-index', this.$refs.menu.listIndex)
      }

      this.$nextTick(() => {
        if (
          !this.internalSearch ||
          (val.length !== 1 &&
            !this.autoSelectFirst)
        ) return

        this.$refs.menu.getTiles()

        if (this.autoSelectFirst && val.length) {
          this.setMenuIndex(0)
          this.$emit('update:list-index', this.$refs.menu.listIndex)
        }
      })
    },
    onInternalSearchChanged () {
      this.updateMenuDimensions()
    },
    updateMenuDimensions () {
      // Type from menuable is not making it through
      this.isMenuActive && this.$refs.menu && this.$refs.menu.updateDimensions()
    },
    changeSelectedIndex (keyCode: number) {
      // Do not allow changing of selectedIndex
      // when search is dirty
      if (this.searchIsDirty) return

      if (this.multiple && keyCode === keyCodes.left) {
        if (this.selectedIndex === -1) {
          this.selectedIndex = this.selectedItems.length - 1
        } else {
          this.selectedIndex--
        }
      } else if (this.multiple && keyCode === keyCodes.right) {
        if (this.selectedIndex >= this.selectedItems.length - 1) {
          this.selectedIndex = -1
        } else {
          this.selectedIndex++
        }
      } else if (keyCode === keyCodes.backspace || keyCode === keyCodes.delete) {
        this.deleteCurrentItem()
      }
    },
    deleteCurrentItem () {
      const curIndex = this.selectedIndex
      const curItem = this.selectedItems[curIndex]

      // Do nothing if input or item is disabled
      if (
        !this.isInteractive ||
        this.getDisabled(curItem)
      ) return

      const lastIndex = this.selectedItems.length - 1

      // Select the last item if
      // there is no selection
      if (
        this.selectedIndex === -1 &&
        lastIndex !== 0
      ) {
        this.selectedIndex = lastIndex

        return
      }

      const length = this.selectedItems.length
      const nextIndex = curIndex !== length - 1
        ? curIndex
        : curIndex - 1
      const nextItem = this.selectedItems[nextIndex]

      if (!nextItem) {
        this.setValue(this.multiple ? [] : null)
      } else {
        this.selectItem(curItem)
      }

      this.selectedIndex = nextIndex
    },
    clearableCallback () {
      this.internalSearch = null

      VSelect.options.methods.clearableCallback.call(this)
    },
    genInput () {
      const input = VTextField.options.methods.genInput.call(this)

      input.data = mergeData(input.data!, {
        attrs: {
          'aria-activedescendant': getObjectValueByPath(this.$refs.menu, 'activeTile.id'),
          autocomplete: getObjectValueByPath(input.data!, 'attrs.autocomplete', 'off'),
        },
        domProps: { value: this.internalSearch },
      })

      return input
    },
    genInputSlot () {
      const slot = VSelect.options.methods.genInputSlot.call(this)

      slot.data!.attrs!.role = 'combobox'

      return slot
    },
    genSelections (): VNode | never[] {
      return this.hasSlot || this.multiple
        ? VSelect.options.methods.genSelections.call(this)
        : []
    },
    onClick (e: MouseEvent) {
      if (!this.isInteractive) return

      this.selectedIndex > -1
        ? (this.selectedIndex = -1)
        : this.onFocus()

      if (!this.isAppendInner(e.target)) this.activateMenu()
    },
    onInput (e: Event) {
      if (
        this.selectedIndex > -1 ||
        !e.target
      ) return

      const target = e.target as HTMLInputElement
      const value = target.value

      // If typing and menu is not currently active
      if (target.value) this.activateMenu()

      if (!this.multiple && value === '') this.deleteCurrentItem()

      this.internalSearch = value
      this.badInput = target.validity && target.validity.badInput
    },
    onKeyDown (e: KeyboardEvent) {
      const keyCode = e.keyCode

      if (
        e.ctrlKey ||
        ![keyCodes.home, keyCodes.end].includes(keyCode)
      ) {
        VSelect.options.methods.onKeyDown.call(this, e)
      }

      // The ordering is important here
      // allows new value to be updated
      // and then moves the index to the
      // proper location
      this.changeSelectedIndex(keyCode)
    },
    onSpaceDown (e: KeyboardEvent) { /* noop */ },
    onTabDown (e: KeyboardEvent) {
      VSelect.options.methods.onTabDown.call(this, e)
      this.updateSelf()
    },
    onUpDown (e: Event) {
      // Prevent screen from scrolling
      e.preventDefault()

      // For autocomplete / combobox, cycling
      // interfers with native up/down behavior
      // instead activate the menu
      this.activateMenu()
    },
    selectItem (item: object) {
      VSelect.options.methods.selectItem.call(this, item)
      this.setSearch()
    },
    setSelectedItems () {
      VSelect.options.methods.setSelectedItems.call(this)

      // #4273 Don't replace if searching
      // #4403 Don't replace if focused
      if (!this.isFocused) this.setSearch()
    },
    setSearch () {
      // Wait for nextTick so selectedItem
      // has had time to update
      this.$nextTick(() => {
        if (
          !this.multiple ||
          !this.internalSearch ||
          !this.isMenuActive
        ) {
          this.internalSearch = (
            !this.selectedItems.length ||
            this.multiple ||
            this.hasSlot
          )
            ? null
            : this.getText(this.selectedItem)
        }
      })
    },
    updateSelf () {
      if (
        !this.searchIsDirty &&
        !this.internalValue
      ) return

      if (
        !this.multiple &&
        !this.valueComparator(
          this.internalSearch,
          this.getValue(this.internalValue)
        )
      ) {
        this.setSearch()
      }
    },
    hasItem (item: any): boolean {
      return this.selectedValues.indexOf(this.getValue(item)) > -1
    },
    onCopy (event: ClipboardEvent) {
      if (this.selectedIndex === -1) return

      const currentItem = this.selectedItems[this.selectedIndex]
      const currentItemText = this.getText(currentItem)
      event.clipboardData?.setData('text/plain', currentItemText)
      event.clipboardData?.setData('text/vnd.vuetify.autocomplete.item+plain', currentItemText)
      event.preventDefault()
    },
  },
})


*  - 'scoped' for old style scoped slots (`<template slot="default" slot-scope="data">`) or bound v-slot (`#default="data"`)

*  - 'v-slot' for unbound v-slot (`#default`) - only if the third param is true, otherwise counts as scoped

export function getSlotType<T extends boolean = false> (vm: Vue, name: string, split?: T): (T extends true ? 'v-slot' : never) | 'normal' | 'scoped' | void {

if (vm.$slots[name] && vm.$scopedSlots[name] && (vm.$scopedSlots[name] as any).name) {

return split ? 'v-slot' as any : 'scoped'

if (vm.$slots.hasOwnProperty(name) && vm.$scopedSlots.hasOwnProperty(name) && (vm.$scopedSlots[name] as any).name) {


// Component
import VVirtualScroll from '../VVirtualScroll'

// Utilities
import {
  shallowMount,
  Wrapper,
} from '@vue/test-utils'

describe('VVirtualScroll.ts', () => {
  type Instance = InstanceType<typeof VVirtualScroll>
  let mountFunction: (options?: object) => Wrapper<Instance>
  let propsData: Object
  let mock: jest.SpyInstance
  const elementHeight = 100

  beforeEach(() => {
    mountFunction = (options = {}) => {
      return shallowMount(VVirtualScroll, {
        ...options,
        scopedSlots: {
          default ({ item }) {
            return this.$createElement('div', { class: 'item' }, item)
          },
        },
      })
    }
    propsData = {
      height: elementHeight,
      items: [1, 2, 3],
      itemHeight: 50,
    }

    // mock clientHeight
    mock = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(elementHeight)
  })

  afterEach(() => {
    mock.mockRestore()
  })

  it('should render component with scopedSlot and match snapshot', async () => {
    const wrapper = mountFunction({
      propsData,
    })

    await wrapper.vm.$nextTick()
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should set height of scrollable element', () => {
    const wrapper = mountFunction({
      propsData,
    })

    const scrollable = wrapper.find('.v-virtual-scroll__container')
    expect(scrollable.element.style.height).toEqual('150px')
  })

  it('should render not more than 5 hidden items and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        height: elementHeight,
        items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
        itemHeight: 50,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render right items on scroll and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        height: elementHeight,
        items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
        itemHeight: 50,
      },
    })

    wrapper.vm.scrollTop = 500
    wrapper.trigger('scroll')

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should provide the correct item index', () => {
    const helpers = require('../../../util/helpers')
    const spy = jest.spyOn(helpers, 'getSlot')
    const wrapper = mountFunction({
      propsData,
      computed: { firstToRender: () => 2 },
    })

    wrapper.setData({ first: 2 })

    wrapper.vm.genChild(0, 1)

    expect(spy.mock.calls[0][2]).toEqual({
      item: 0,
      index: 3,
    })
  })
})


const path = require('path')
const merge = require('webpack-merge')
const HappyPack = require('happypack')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { VuetifyProgressiveModule } = require('vuetify-loader')
const { config: baseWebpackConfig, happyThreadPool } = require('./webpack.base.config')

// Helpers
const resolve = file => path.resolve(__dirname, file)

module.exports = merge(baseWebpackConfig, {
  devtool: 'source-map',
  entry: ['babel-polyfill', './dev/index.js'],
  output: {
    filename: '[name].js',
    path: resolve('../dev'),
    publicPath: '/dev/',
    library: 'Vuetify',
  },
  resolve: {
    alias: {
      vuetify: resolve('../src'),
      vue$: 'vue/dist/vue.esm.js',
    },
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            modules: [VuetifyProgressiveModule],
          },
        },
      },
      {
        test: /\.ts$/,
        use: 'happypack/loader?id=ts',
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        use: 'happypack/loader?id=js',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/,
        oneOf: [
          {
            test: /\.(png|jpe?g|gif)$/,
            resourceQuery: /vuetify-preload/,
            use: [
              'vuetify-loader/progressive-loader',
              {
                loader: 'url-loader',
                options: { limit: 8000 },
              },
            ],
          },
          {
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: 'img/[name].[hash:7].[ext]',
            },
          },
        ],
      },
    ],
  },
  devServer: {
    contentBase: resolve('../dev'),
    publicPath: '/dev/',
    host: process.env.HOST || 'localhost',
    port: process.env.PORT || '8080',
    disableHostCheck: true,
  },
  plugins: [
    new VueLoaderPlugin(),
    new ForkTsCheckerWebpackPlugin({
      checkSyntacticErrors: true,
      tsconfig: resolve('../tsconfig.json'),
    }),
    new HappyPack({
      id: 'ts',
      threadPool: happyThreadPool,
      loaders: [
        'babel-loader',
        {
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/],
            happyPackMode: true,
          },
        },
        'eslint-loader?cache=true?emitWarning=true',
      ],
    }),
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: ['babel-loader', 'eslint-loader?cache=true?emitWarning=true'],
    }),
  ],
})


<template>
  <v-card>
    <v-tabs
      v-model="tab"
      background-color="primary"
      dark
    >
      <v-tab
        v-for="item in items"
        :key="item.tab"
      >
        {{ item.tab }}
      </v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item
        v-for="item in items"
        :key="item.tab"
      >
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>

<script>
  export default {
    data () {
      return {
        tab: null,
        items: [
          { tab: 'One', content: 'Tab 1 Content' },
          { tab: 'Two', content: 'Tab 2 Content' },
          { tab: 'Three', content: 'Tab 3 Content' },
          { tab: 'Four', content: 'Tab 4 Content' },
          { tab: 'Five', content: 'Tab 5 Content' },
          { tab: 'Six', content: 'Tab 6 Content' },
          { tab: 'Seven', content: 'Tab 7 Content' },
          { tab: 'Eight', content: 'Tab 8 Content' },
          { tab: 'Nine', content: 'Tab 9 Content' },
          { tab: 'Ten', content: 'Tab 10 Content' },
        ],
      }
    },
  }
</script>


if (vm.$scopedSlots[name]) return 'scoped'

export function debounce (fn: Function, delay: number) {

keep keep replace replace keep keep keep

if (vm.$slots.hasOwnProperty(name)) return 'normal'

if (vm.$scopedSlots.hasOwnProperty(name)) return 'scoped'

// Helpers
import { VNode, VNodeData } from 'vue'
import mixins from '../../util/mixins'
import header from './mixins/header'
import { wrapInArray, convertToUnit } from '../../util/helpers'
import { DataTableHeader } from 'vuetify/types'

export default mixins(header).extend({
  name: 'v-data-table-header-desktop',

  methods: {
    genGroupByToggle (header: DataTableHeader) {
      return this.$createElement('span', {
        on: {
          click: (e: MouseEvent) => {
            e.stopPropagation()
            this.$emit('group', header.value)
          },
        },
      }, ['group'])
    },
    getAria (beingSorted: boolean, isDesc: boolean) {
      const $t = (key: string) => this.$vuetify.lang.t(`$vuetify.dataTable.ariaLabel.${key}`)

      let ariaSort = 'none'
      let ariaLabel = [
        $t('sortNone'),
        $t('activateAscending'),
      ]

      if (!beingSorted) {
        return { ariaSort, ariaLabel: ariaLabel.join(' ') }
      }

      if (isDesc) {
        ariaSort = 'descending'
        ariaLabel = [
          $t('sortDescending'),
          $t(this.options.mustSort ? 'activateAscending' : 'activateNone'),
        ]
      } else {
        ariaSort = 'ascending'
        ariaLabel = [
          $t('sortAscending'),
          $t('activateDescending'),
        ]
      }

      return { ariaSort, ariaLabel: ariaLabel.join(' ') }
    },
    genHeader (header: DataTableHeader) {
      const data: Required<Pick<VNodeData, 'attrs' | 'on' | 'class' | 'style'>> = {
        attrs: {
          role: 'columnheader',
          scope: 'col',
          'aria-label': header.text || '',
        },
        style: {
          width: convertToUnit(header.width),
          minWidth: convertToUnit(header.width),
        },
        class: [
          `text-${header.align || 'start'}`,
          ...wrapInArray(header.class),
          header.divider && 'v-data-table__divider',
        ],
        on: {},
      }
      const children = []

      if (header.value === 'data-table-select' && !this.singleSelect) {
        return this.$createElement('th', data, [this.genSelectAll()])
      }

      children.push(
        this.$scopedSlots[header.value]
          ? this.$scopedSlots[header.value]!({ header })
          : this.$createElement('span', [header.text])
      )

      if (!this.disableSort && (header.sortable || !header.hasOwnProperty('sortable'))) {
        data.on.click = () => this.$emit('sort', header.value)

        const sortIndex = this.options.sortBy.findIndex(k => k === header.value)
        const beingSorted = sortIndex >= 0
        const isDesc = this.options.sortDesc[sortIndex]

        data.class.push('sortable')

        const { ariaLabel, ariaSort } = this.getAria(beingSorted, isDesc)

        data.attrs['aria-label'] += `${header.text ? ': ' : ''}${ariaLabel}`
        data.attrs['aria-sort'] = ariaSort

        if (beingSorted) {
          data.class.push('active')
          data.class.push(isDesc ? 'desc' : 'asc')
        }

        if (header.align === 'end') children.unshift(this.genSortIcon())
        else children.push(this.genSortIcon())

        if (this.options.multiSort && beingSorted) {
          children.push(this.$createElement('span', { class: 'v-data-table-header__sort-badge' }, [String(sortIndex + 1)]))
        }
      }

      if (this.showGroupBy && header.groupable !== false) children.push(this.genGroupByToggle(header))

      return this.$createElement('th', data, children)
    },
  },

  render (): VNode {
    return this.$createElement('thead', {
      staticClass: 'v-data-table-header',
    }, [
      this.$createElement('tr', this.headers.map(header => this.genHeader(header))),
    ])
  },
})


// Adds leading zero to month/day if necessary, returns 'YYYY' if type = 'year',
// 'YYYY-MM' if 'month' and 'YYYY-MM-DD' if 'date'
import pad from './pad'

export default (dateString: string, type: 'date' | 'month' | 'year'): string => {
  const [year, month = 1, date = 1] = dateString.split('-')
  return `${year}-${pad(month)}-${pad(date)}`.substr(0, { date: 10, month: 7, year: 4 }[type])
}


<template>
  <v-simple-table>
    <template v-slot:default>

            Name

            Calories

          {{ item.name }}
          {{ item.calories }}

    </template>
  </v-simple-table>
</template>


module.exports = {
  'v-lazy': {
    slots: [
      {
        name: 'default',
        props: undefined,
      },
    ],
  },
}


export function getSlot (vm: Vue, name = 'default', data?: object | (() => object), optional = false) {

return vm.$scopedSlots[name]!(data instanceof Function ? data() : data)


---
meta:
  title: Range Slider component
  description: The range slider component is a better visualization of the number input. It is used for gathering a range of numerical user data.
  keywords: sliders, range, vuetify slider component, vuetify range slider component, vue slider component
related:
  - /components/forms/
  - /components/selects/
  - /components/sliders/
---

# Range Sliders

The `v-slider` component is a better visualization of the number input. It is used for gathering numerical user data.

<entry-ad />

## Usage

Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters.

<usage name="v-range-slider" />

## API

- [v-range-slider](/api/v-range-slider)
- [v-slider](/api/v-slider)

<inline-api page="components/range-sliders" />

## Examples

### Props

#### Disabled

You cannot interact with **disabled** sliders.

<example file="v-range-slider/prop-disabled" />

#### Min and max

You can set **min** and **max** values of sliders.

<example file="v-range-slider/prop-min-and-max" />

#### Step

`v-range-slider` can have steps other than 1. This can be helpful for some applications where you need to adjust values with more or less accuracy.

<example file="v-range-slider/prop-step" />

#### Vertical sliders

You can use the **vertical** prop to switch sliders to a vertical orientation. If you need to change the height of the slider, use css.

<example file="v-range-slider/prop-vertical" />

### Slots

#### Thumb label

Using the **tick-labels** prop along with the `thumb-label` slot, you can create a very customized solution.

<example file="v-range-slider/slot-thumb-label" />

<backmatter />


<template>
  <v-form>
    <v-container>
      <v-row>
        <v-col
          cols="12"
          sm="6"
        >
          <v-text-field
            label="Prepend"
            prepend-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            label="Prepend inner"
            prepend-inner-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            label="Append"
            append-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            label="Append outer"
            append-outer-icon="mdi-map-marker"
          ></v-text-field>
        </v-col>

        <v-col
          cols="12"
          sm="6"
        >
          <v-text-field
            solo
            label="Prepend"
            prepend-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            solo
            label="Prepend inner"
            prepend-inner-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            solo
            label="Append"
            append-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            solo
            label="Append outer"
            append-outer-icon="mdi-map-marker"
          ></v-text-field>
        </v-col>

        <v-col
          cols="12"
          sm="6"
        >
          <v-text-field
            filled
            label="Prepend"
            prepend-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            filled
            label="Prepend inner"
            prepend-inner-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            filled
            label="Append"
            append-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            filled
            label="Append outer"
            append-outer-icon="mdi-map-marker"
          ></v-text-field>
        </v-col>

        <v-col
          cols="12"
          sm="6"
        >
          <v-text-field
            outlined
            label="Prepend"
            prepend-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            outlined
            label="Prepend inner"
            prepend-inner-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            outlined
            label="Append"
            append-icon="mdi-map-marker"
          ></v-text-field>

          <v-text-field
            outlined
            label="Append outer"
            append-outer-icon="mdi-map-marker"
          ></v-text-field>
        </v-col>
      </v-row>
    </v-container>
  </v-form>
</template>