mirror of https://github.com/nextcloud/contacts
Vue cleanup and init
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
3f3ad0eeb8
commit
75f0d3c093
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 11"]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -1,16 +1,9 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{html,js,css}]
|
||||
indent_style = tab
|
||||
|
||||
trim_trailing_whitespace = true
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
globals: {
|
||||
t: false,
|
||||
n: false,
|
||||
OC: false,
|
||||
OCA: false
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:node/recommended',
|
||||
'plugin:vue/recommended',
|
||||
'standard'
|
||||
],
|
||||
plugins: ['vue', 'node'],
|
||||
rules: {
|
||||
// space before function ()
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
// curly braces always space
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
// stay consistent with array brackets
|
||||
'array-bracket-newline': ['error', 'consistent'],
|
||||
// 1tbs brace style
|
||||
'brace-style': 'error',
|
||||
// tabs only
|
||||
indent: ['error', 'tab'],
|
||||
'no-tabs': 0,
|
||||
// es6 import/export and require
|
||||
'node/no-unpublished-require': ['off'],
|
||||
'node/no-unsupported-features': ['off'],
|
||||
// vue format
|
||||
'vue/html-indent': ['error', 'tab'],
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
singleline: 3,
|
||||
multiline: {
|
||||
max: 3,
|
||||
allowFirstLine: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"eqeqeq": ["warn", "smart"],
|
||||
"no-console": "warn",
|
||||
"no-loop-func": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
|
||||
"block-spacing": "error",
|
||||
"camelcase": "error",
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
"curly": ["error", "multi-line", "consistent"],
|
||||
"indent": ["error", "tab"],
|
||||
"no-alert": "error",
|
||||
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
||||
"no-trailing-spaces": "error",
|
||||
"quotes": ["error", "single", "avoid-escape"],
|
||||
"semi": "error",
|
||||
"space-before-blocks": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jquery": true
|
||||
},
|
||||
"globals": {
|
||||
"angular": false,
|
||||
"vCard": false,
|
||||
"_": false,
|
||||
"OC": false,
|
||||
"t": false,
|
||||
"n": false,
|
||||
"dav": false,
|
||||
"OCA": false
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
node_modules
|
||||
build
|
||||
js/public
|
||||
js/vendor
|
||||
css/vendor
|
||||
css/style.css
|
||||
coverage
|
||||
npm-debug.log
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
40
.stylelintrc
40
.stylelintrc
|
@ -1,22 +1,22 @@
|
|||
{
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"indentation": "tab",
|
||||
"selector-type-no-unknown": null,
|
||||
"number-leading-zero": null,
|
||||
"rule-empty-line-before": ["always", {
|
||||
"ignore": ["after-comment", "inside-block"]
|
||||
}],
|
||||
"declaration-empty-line-before": ["never", {
|
||||
"ignore": ["after-declaration"]
|
||||
}],
|
||||
"comment-empty-line-before": null,
|
||||
"selector-type-case": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"no-descending-specificity": null,
|
||||
"string-quotes": "single",
|
||||
},
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
]
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"indentation": "tab",
|
||||
"selector-type-no-unknown": null,
|
||||
"number-leading-zero": null,
|
||||
"rule-empty-line-before": ["always", {
|
||||
"ignore": ["after-comment", "inside-block"]
|
||||
}],
|
||||
"declaration-empty-line-before": ["never", {
|
||||
"ignore": ["after-declaration"]
|
||||
}],
|
||||
"comment-empty-line-before": null,
|
||||
"selector-type-case": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"no-descending-specificity": null,
|
||||
"string-quotes": "single"
|
||||
},
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
]
|
||||
}
|
||||
|
|
90
.travis.yml
90
.travis.yml
|
@ -1,90 +0,0 @@
|
|||
sudo: false
|
||||
dist: trusty
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.1
|
||||
# - 7.2
|
||||
env:
|
||||
global:
|
||||
- CORE_BRANCH=master
|
||||
- NIGHTLY=FALSE
|
||||
matrix:
|
||||
- DB=pgsql
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^stable\d+(\.\d+)?$/
|
||||
- /^v\d++(\.\d+)?+(\.\d+)?+(\.\d+)?$/
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 7.0
|
||||
env: DB=pgsql CORE_BRANCH=stable12
|
||||
- php: 7.1
|
||||
env: DB=pgsql CORE_BRANCH=stable12
|
||||
- php: 7.0
|
||||
env: DB=pgsql CORE_BRANCH=stable13
|
||||
- php: 7.1
|
||||
env: DB=pgsql CORE_BRANCH=stable13
|
||||
# - php: 7.2
|
||||
# env: DB=pgsql CORE_BRANCH=stable13
|
||||
fast_finish: true
|
||||
|
||||
before_install:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- nvm install 7.6
|
||||
- npm install -g npm@latest
|
||||
- make
|
||||
- make appstore
|
||||
# install nextcloud
|
||||
- cd ../
|
||||
- git clone https://github.com/nextcloud/server.git --recursive --depth 1 -b $CORE_BRANCH core
|
||||
- mv contacts core/apps/
|
||||
|
||||
before_script:
|
||||
- if [[ "$DB" == 'pgsql' ]]; then createuser -U travis -s oc_autotest; fi
|
||||
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e 'create database oc_autotest;'; fi
|
||||
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "CREATE USER 'oc_autotest'@'localhost' IDENTIFIED BY '';"; fi
|
||||
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "grant all on oc_autotest.* to 'oc_autotest'@'localhost';"; fi
|
||||
# fill nextcloud with default configs and enable contacts
|
||||
- cd core
|
||||
- mkdir data
|
||||
- ./occ maintenance:install --database-name oc_autotest --database-user oc_autotest --admin-user admin --admin-pass admin --database $DB --database-pass=''
|
||||
- ./occ app:enable contacts
|
||||
- ./occ background:cron # enable default cron
|
||||
- php -S localhost:8080 &
|
||||
- cd apps/contacts
|
||||
|
||||
script:
|
||||
- make test
|
||||
|
||||
# Upload the nightly to ftp server
|
||||
- if [[ "$NIGHTLY" = "TRUE" ]]; then curl --ftp-create-dirs -T /home/travis/build/nextcloud/core/apps/contacts/build/artifacts/appstore/contacts.tar.gz -u $FTP_LOGIN:$FTP_PW ftp://upload.portknox.de/htdocs/contacts/nextcloud_contacts_nightly_build_$(date +%Y-%m-%d).tar.gz; fi
|
||||
|
||||
after_failure:
|
||||
- cat ../../data/nextcloud.log
|
||||
|
||||
after_success:
|
||||
# codecov has issues when not run exactly in the cloned folder on travis, so
|
||||
# revert everything
|
||||
- cd ../../../
|
||||
- mv core/apps/contacts .
|
||||
- cd contacts
|
||||
- node_modules/codecov/bin/codecov
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: IBNQc4MsBqOc6bj2fD1PnMFfELFpP2GqpZjmBsqP43dWixo8vZzafg7JwlsfnuC0rfcOE/2NwHQl43d+37sXMbMl+ZXgz2ax/wOyLAS2PK/EQEDkzYOdI0E/8u2D7V7m9LlCQ8MOGSGmjYwFcDLzcmgU8AOWg4N85ktpOkaiVF1287Rr+wcRZ0o4/VTVvykYzfKDIBaACAX+EaXtpBtD0cTr1lFN4vKuUma2+iX+MyPVZcvIbCWv2bTzqXzfkT3NagZuFXcooXhvPGFoOb8AisxRSoVP48Vpt8ziG+7wDFlIrNe+fjNJxOEMDEP8cYljoUU6MaOxcm012s/CqHjWBuTI5MRAWlH4w9YJ/1BhFoSJOUb21401zp255puPZJ+Vq8i720F21xm7g7Vc/1NsEAwmTzLgaG8cnV98SonITVDuR4qIaMWpHwTMhap6sHMW7UfH4QnCKypo1mgITFdjM9ANYbcfF8GBfrK4MZtuw75AcLoytFia+KnAOO7OpC93eo6Czcqu6ILOBz1XNWZcFQJTrkLKkFslZLhSSrgPrTL4Py0zVmBurxdOmoZkDcxyKmk/1ggQmZKhh7OS1TGW/7tckscwMhukLwnQiXBCQJ7VWAJ/2eaolym1+fDbqJ4z8t9q2KEfZyqlYAL4VxPqQzxwO9O19ej1WtncvpFHlQw=
|
||||
file: build/artifacts/appstore/contacts.tar.gz
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: nextcloud/contacts
|
||||
tags: true
|
||||
php: 7.0
|
||||
|
||||
addons:
|
||||
firefox: "latest"
|
208
Makefile
208
Makefile
|
@ -1,182 +1,46 @@
|
|||
# This file is licensed under the Affero General Public License version 3 or
|
||||
# later. See the COPYING file.
|
||||
# @author Bernhard Posselt <dev@bernhard-posselt.com>
|
||||
# @copyright Bernhard Posselt 2016
|
||||
all: dev-setup lint build-js-production test
|
||||
|
||||
# Generic Makefile for building and packaging an Nextcloud app which uses npm and
|
||||
# Composer.
|
||||
#
|
||||
# Dependencies:
|
||||
# * make
|
||||
# * which
|
||||
# * curl: used if phpunit and composer are not installed to fetch them from the web
|
||||
# * tar: for building the archive
|
||||
# * npm: for building and testing everything JS
|
||||
#
|
||||
# If no composer.json is in the app root directory, the Composer step
|
||||
# will be skipped. The same goes for the package.json which can be located in
|
||||
# the app root or the js/ directory.
|
||||
#
|
||||
# The npm command by launches the npm build script:
|
||||
#
|
||||
# npm run build
|
||||
#
|
||||
# The npm test command launches the npm test script:
|
||||
#
|
||||
# npm run test
|
||||
#
|
||||
# The idea behind this is to be completely testing and build tool agnostic. All
|
||||
# build tools and additional package managers should be installed locally in
|
||||
# your project, since this won't pollute people's global namespace.
|
||||
#
|
||||
# The following npm scripts in your package.json install and update the bower
|
||||
# and npm dependencies and use gulp as build system (notice how everything is
|
||||
# run from the node_modules folder):
|
||||
#
|
||||
# "scripts": {
|
||||
# "test": "node node_modules/gulp-cli/bin/gulp.js karma",
|
||||
# "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
|
||||
# "build": "node node_modules/gulp-cli/bin/gulp.js"
|
||||
# },
|
||||
# Dev env management
|
||||
dev-setup: clean clean-dev npm-init
|
||||
|
||||
app_name=$(notdir $(CURDIR))
|
||||
project_directory=$(CURDIR)/../$(app_name)
|
||||
build_tools_directory=$(CURDIR)/build/tools
|
||||
source_build_directory=$(CURDIR)/build/artifacts/source
|
||||
source_package_name=$(source_build_directory)/$(app_name)
|
||||
appstore_build_directory=$(CURDIR)/build/artifacts/appstore
|
||||
appstore_package_name=$(appstore_build_directory)/$(app_name)
|
||||
npm=$(shell which npm 2> /dev/null)
|
||||
composer=$(shell which composer 2> /dev/null)
|
||||
npm-init:
|
||||
npm install
|
||||
|
||||
all: build
|
||||
npm-update:
|
||||
npm update
|
||||
|
||||
# Fetches the PHP and JS dependencies and compiles the JS. If no composer.json
|
||||
# is present, the composer step is skipped, if no package.json or js/package.json
|
||||
# is present, the npm step is skipped
|
||||
.PHONY: build
|
||||
build:
|
||||
ifneq (,$(wildcard $(CURDIR)/composer.json))
|
||||
make composer
|
||||
endif
|
||||
ifneq (,$(wildcard $(CURDIR)/package.json))
|
||||
make npm
|
||||
endif
|
||||
ifneq (,$(wildcard $(CURDIR)/js/package.json))
|
||||
make npm
|
||||
endif
|
||||
# Building
|
||||
build-js:
|
||||
npm run dev
|
||||
|
||||
# Installs and updates the composer dependencies. If composer is not installed
|
||||
# a copy is fetched from the web
|
||||
.PHONY: composer
|
||||
composer:
|
||||
ifeq (, $(composer))
|
||||
@echo "No composer command available, downloading a copy from the web"
|
||||
mkdir -p $(build_tools_directory)
|
||||
curl -sS https://getcomposer.org/installer | php
|
||||
mv composer.phar $(build_tools_directory)
|
||||
php $(build_tools_directory)/composer.phar install --prefer-dist
|
||||
php $(build_tools_directory)/composer.phar update --prefer-dist
|
||||
else
|
||||
composer install --prefer-dist
|
||||
composer update --prefer-dist
|
||||
endif
|
||||
|
||||
# We need to build css files for Nextcloud 11
|
||||
# variables.scss is necessary and not provided by stable11 => download it
|
||||
.PHONY: css
|
||||
css:
|
||||
ifeq (,$(wildcard $(CURDIR)/build/css/variables.scss))
|
||||
curl --silent --create-dirs -o $(CURDIR)/build/css/variables.scss https://raw.githubusercontent.com/nextcloud/server/master/core/css/variables.scss
|
||||
npm run scss-compile
|
||||
else
|
||||
npm run scss-compile
|
||||
endif
|
||||
|
||||
# Installs npm dependencies
|
||||
.PHONY: npm
|
||||
npm:
|
||||
ifeq (,$(wildcard $(CURDIR)/package.json))
|
||||
cd js && $(npm) run build
|
||||
else
|
||||
build-js-production:
|
||||
npm run build
|
||||
make css
|
||||
endif
|
||||
|
||||
# Removes the appstore build
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf ./build
|
||||
watch-js:
|
||||
npm run watch
|
||||
|
||||
# Same as clean but also removes dependencies installed by composer, bower and
|
||||
# npm
|
||||
.PHONY: distclean
|
||||
distclean: clean
|
||||
rm -rf vendor
|
||||
rm -rf node_modules
|
||||
rm -rf js/vendor
|
||||
rm -rf js/node_modules
|
||||
|
||||
# Builds the source and appstore package
|
||||
.PHONY: dist
|
||||
dist:
|
||||
make source
|
||||
make appstore
|
||||
|
||||
# Builds the source package
|
||||
.PHONY: source
|
||||
source:
|
||||
rm -rf $(source_build_directory)
|
||||
mkdir -p $(source_build_directory)
|
||||
tar cvzf $(source_package_name).tar.gz ../$(app_name) \
|
||||
--exclude-vcs \
|
||||
--exclude="../$(app_name)/build" \
|
||||
--exclude="../$(app_name)/js/node_modules" \
|
||||
--exclude="../$(app_name)/node_modules" \
|
||||
--exclude="../$(app_name)/*.log" \
|
||||
--exclude="../$(app_name)/js/*.log" \
|
||||
|
||||
# Builds the source package for the app store, ignores php and js tests
|
||||
.PHONY: appstore
|
||||
appstore:
|
||||
rm -rf $(appstore_build_directory)
|
||||
mkdir -p $(appstore_build_directory)
|
||||
tar cvzf $(appstore_package_name).tar.gz \
|
||||
--exclude-vcs \
|
||||
$(project_directory)/appinfo \
|
||||
$(project_directory)/css \
|
||||
$(project_directory)/img \
|
||||
$(project_directory)/l10n \
|
||||
$(project_directory)/lib \
|
||||
$(project_directory)/templates \
|
||||
$(project_directory)/js/public \
|
||||
$(project_directory)/js/vendor \
|
||||
$(project_directory)/js/dav/dav.js
|
||||
|
||||
# Command for running JS and PHP tests. Works for package.json files in the js/
|
||||
# and root directory. If phpunit is not installed systemwide, a copy is fetched
|
||||
# from the internet
|
||||
.PHONY: test
|
||||
# Testing
|
||||
test:
|
||||
ifneq (,$(wildcard $(CURDIR)/js/package.json))
|
||||
cd js && $(npm) run test
|
||||
endif
|
||||
ifneq (,$(wildcard $(CURDIR)/package.json))
|
||||
$(npm) run test
|
||||
endif
|
||||
# hotfix to prevent travis from using phpunix 6.x
|
||||
@echo "No phpunit command available, downloading a copy from the web"
|
||||
mkdir -p $(build_tools_directory)
|
||||
curl -sSL https://phar.phpunit.de/phpunit-5.7.9.phar -o $(build_tools_directory)/phpunit.phar
|
||||
php $(build_tools_directory)/phpunit.phar -c phpunit.xml --coverage-clover build/php-unit.clover
|
||||
php $(build_tools_directory)/phpunit.phar -c phpunit.integration.xml --coverage-clover build/php-integration.clover
|
||||
npm run test
|
||||
|
||||
test-watch:
|
||||
npm run test:watch
|
||||
|
||||
test-coverage:
|
||||
npm run test:coverage
|
||||
|
||||
# Linting
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
lint-fix:
|
||||
npm run lint:fix
|
||||
|
||||
# Cleaning
|
||||
clean:
|
||||
rm -f js/contacts.js
|
||||
rm -f js/contacts.js.map
|
||||
|
||||
clean-dev:
|
||||
rm -rf node_modules
|
||||
|
||||
# watch out for changes and rebuild
|
||||
.PHONY: watch
|
||||
watch:
|
||||
ifneq (,$(wildcard $(CURDIR)/js/package.json))
|
||||
cd js && $(npm) run watch
|
||||
endif
|
||||
ifneq (,$(wildcard $(CURDIR)/package.json))
|
||||
$(npm) run watch
|
||||
endif
|
||||
|
|
81
README.md
81
README.md
|
@ -1,74 +1,23 @@
|
|||
# Nextcloud Contacts
|
||||
# contacts
|
||||
|
||||
![Downloads](https://img.shields.io/github/downloads/nextcloud/contacts/total.svg)
|
||||
[![irc](https://img.shields.io/badge/IRC-%23nextcloud--contacts%20on%20freenode-blue.svg)](https://webchat.freenode.net/?channels=nextcloud-contacts)
|
||||
[![Build Status](https://travis-ci.org/nextcloud/contacts.svg?branch=master)](https://travis-ci.org/nextcloud/contacts)
|
||||
[![Code coverage](https://img.shields.io/codecov/c/github/nextcloud/contacts.svg?maxAge=2592000)](https://codecov.io/gh/nextcloud/contacts/)
|
||||
> A contacts app for Nextcloud. Easily sync contacts from various devices, share and edit them online.
|
||||
|
||||
**A contacts app for [Nextcloud](https://nextcloud.com). Easily sync contacts from various devices with your Nextcloud and edit them online.**
|
||||
## Build Setup
|
||||
|
||||
![](https://raw.githubusercontent.com/nextcloud/screenshots/master/apps/Contacts/contacts.png)
|
||||
``` bash
|
||||
# set up and build for production
|
||||
make
|
||||
|
||||
## Why is this so awesome?
|
||||
# install dependencies
|
||||
make dev-setup
|
||||
|
||||
* :rocket: **Integration with other Nextcloud apps!** Currently Mail and Calendar – more to come.
|
||||
* :tada: **Never forget a birthday!** You can sync birthdays and other recurring events with your Nextcloud Calendar.
|
||||
* :busts_in_silhouette: **Sharing of Adressbooks!** You want to share your contacts with your friends or coworkers? No problem!
|
||||
* :see_no_evil: **We’re not reinventing the wheel!** Based on the great and open SabreDAV library.
|
||||
# build for dev and watch changes
|
||||
make watch-js
|
||||
|
||||
## Installation
|
||||
# build for dev
|
||||
make build-js
|
||||
|
||||
In your Nextcloud, simply navigate to »Apps«, choose the category »Organization«, find the Contacts app and enable it.
|
||||
Then open the Contacts app from the app menu.
|
||||
# build for production with minification
|
||||
make build-js-production
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you need assistance or want to ask a question about Contacts, you are welcome to [ask for support](https://help.nextcloud.com) in our Forums or the [IRC-Channel](https://webchat.freenode.net/?channels=nextcloud-contacts). If you have found a bug, feel free to open a new Issue on GitHub. Keep in mind, that this repository only manages the frontend. If you find bugs or have problems with the CardDAV-Backend, you should ask the guys at [Nextcloud server](https://github.com/nextcloud/server) for help!
|
||||
|
||||
## Maintainers:
|
||||
|
||||
- [Hendrik Leppelsack](https://github.com/Henni)
|
||||
- [Jan-Christoph Borchardt](https://github.com/jancborchardt)
|
||||
- [John Molakvoæ](https://github.com/skjnldsv)
|
||||
|
||||
If you'd like to join, just go through the [issue list](https://github.com/nextcloud/contacts/issues) and fix some. :)
|
||||
|
||||
### Nightly builds
|
||||
|
||||
Instead of setting everything up manually, you can just [download the nightly builds](https://nightly.portknox.net/contacts/?C=M;O=D) instead. These builds are updated every 24 hours, and are pre-configured with all the needed dependencies.
|
||||
|
||||
1. Download
|
||||
2. Extract the tar archive to 'path-to-nextcloud/apps'
|
||||
3. Navigate to »Apps«, choose the category »Productivity«, find the Contacts app and enable it.
|
||||
|
||||
The nightly builds are provided by [Portknox.net](https://portknox.net)
|
||||
|
||||
## Building the app
|
||||
|
||||
The app can be built by using the provided Makefile by running:
|
||||
|
||||
make
|
||||
|
||||
This requires the following things to be present:
|
||||
* make
|
||||
* which
|
||||
* tar: for building the archive
|
||||
* curl: used if phpunit and composer are not installed to fetch them from the web
|
||||
* npm: for building and testing everything JS
|
||||
|
||||
## Running tests
|
||||
You can use the provided Makefile to run all tests by using:
|
||||
|
||||
make test
|
||||
|
||||
This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test**
|
||||
|
||||
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly:
|
||||
|
||||
phpunit -c phpunit.xml
|
||||
|
||||
or:
|
||||
|
||||
phpunit -c phpunit.integration.xml
|
||||
|
||||
for integration tests
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Nextcloud - contacts
|
||||
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3 or
|
||||
* later. See the COPYING file.
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Hendrik Leppelsack <hendrik@leppelsack.de>
|
||||
* @copyright Hendrik Leppelsack 2015
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create your routes in here. The name is the lowercase name of the controller
|
||||
* without the controller part, the stuff after the hash is the method.
|
||||
* e.g. page#index -> OCA\Contacts\Controller\PageController->index()
|
||||
*
|
||||
* The controller class has to be registered in the application.php file since
|
||||
* it's instantiated in there
|
||||
*/
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'],
|
||||
]
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#indexGroup', 'url' => '/{group}', 'verb' => 'GET'],
|
||||
['name' => 'page#indexContact', 'url' => '/{group}/{contact}', 'verb' => 'GET']
|
||||
]
|
||||
];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<coverage generated="1531297308235" clover="3.2.0">
|
||||
<project timestamp="1531297308235" name="All files">
|
||||
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0">
|
||||
</metrics>
|
||||
</project>
|
||||
</coverage>
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,223 @@
|
|||
body, html {
|
||||
margin:0; padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, Arial;
|
||||
font-size: 14px;
|
||||
color:#333;
|
||||
}
|
||||
.small { font-size: 12px; }
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0;}
|
||||
h2 { font-size: 14px; }
|
||||
pre {
|
||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
a { color:#0074D9; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.strong { font-weight: bold; }
|
||||
.space-top1 { padding: 10px 0 0 0; }
|
||||
.pad2y { padding: 20px 0; }
|
||||
.pad1y { padding: 10px 0; }
|
||||
.pad2x { padding: 0 20px; }
|
||||
.pad2 { padding: 20px; }
|
||||
.pad1 { padding: 10px; }
|
||||
.space-left2 { padding-left:55px; }
|
||||
.space-right2 { padding-right:20px; }
|
||||
.center { text-align:center; }
|
||||
.clearfix { display:block; }
|
||||
.clearfix:after {
|
||||
content:'';
|
||||
display:block;
|
||||
height:0;
|
||||
clear:both;
|
||||
visibility:hidden;
|
||||
}
|
||||
.fl { float: left; }
|
||||
@media only screen and (max-width:640px) {
|
||||
.col3 { width:100%; max-width:100%; }
|
||||
.hide-mobile { display:none!important; }
|
||||
}
|
||||
|
||||
.quiet {
|
||||
color: #7f7f7f;
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.quiet a { opacity: 0.7; }
|
||||
|
||||
.fraction {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
background: #E8E8E8;
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.path a:link, div.path a:visited { color: #333; }
|
||||
table.coverage {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.coverage td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.coverage td.line-count {
|
||||
text-align: right;
|
||||
padding: 0 5px 0 20px;
|
||||
}
|
||||
table.coverage td.line-coverage {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
min-width:20px;
|
||||
}
|
||||
|
||||
table.coverage td span.cline-any {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
.missing-if-branch {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #333;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.skip-if-branch {
|
||||
display: none;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #ccc;
|
||||
color: white;
|
||||
}
|
||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||
color: inherit !important;
|
||||
}
|
||||
.coverage-summary {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||
.keyline-all { border: 1px solid #ddd; }
|
||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||
.coverage-summary td:last-child { border-right: none; }
|
||||
.coverage-summary th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.coverage-summary th.file { border-right: none !important; }
|
||||
.coverage-summary th.pct { }
|
||||
.coverage-summary th.pic,
|
||||
.coverage-summary th.abs,
|
||||
.coverage-summary td.pct,
|
||||
.coverage-summary td.abs { text-align: right; }
|
||||
.coverage-summary td.file { white-space: nowrap; }
|
||||
.coverage-summary td.pic { min-width: 120px !important; }
|
||||
.coverage-summary tfoot td { }
|
||||
|
||||
.coverage-summary .sorter {
|
||||
height: 10px;
|
||||
width: 7px;
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
.coverage-summary .sorted .sorter {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
.coverage-summary .sorted-desc .sorter {
|
||||
background-position: 0 -10px;
|
||||
}
|
||||
.status-line { height: 10px; }
|
||||
/* yellow */
|
||||
.cbranch-no { background: yellow !important; color: #111; }
|
||||
/* dark red */
|
||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||
.low .chart { border:1px solid #C21F39 }
|
||||
.highlighted,
|
||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||
background: #C21F39 !important;
|
||||
}
|
||||
/* medium red */
|
||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||
/* light red */
|
||||
.low, .cline-no { background:#FCE1E5 }
|
||||
/* light green */
|
||||
.high, .cline-yes { background:rgb(230,245,208) }
|
||||
/* medium green */
|
||||
.cstat-yes { background:rgb(161,215,106) }
|
||||
/* dark green */
|
||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||
.high .chart { border:1px solid rgb(77,146,33) }
|
||||
|
||||
.medium .chart { border:1px solid #666; }
|
||||
.medium .cover-fill { background: #666; }
|
||||
|
||||
.cstat-skip { background: #ddd; color: #111; }
|
||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||
|
||||
span.cline-neutral { background: #eaeaea; }
|
||||
.medium { background: #eaeaea; }
|
||||
|
||||
.coverage-summary td.empty {
|
||||
opacity: .5;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cover-fill, .cover-empty {
|
||||
display:inline-block;
|
||||
height: 12px;
|
||||
}
|
||||
.chart {
|
||||
line-height: 0;
|
||||
}
|
||||
.cover-empty {
|
||||
background: white;
|
||||
}
|
||||
.cover-full {
|
||||
border-right: none !important;
|
||||
}
|
||||
pre.prettyprint {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.com { color: #999 !important; }
|
||||
.ignore-none { color: #999; font-weight: normal; }
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -48px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 48px;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
var jumpToCode = (function init () {
|
||||
// Classes of code we would like to highlight
|
||||
var missingCoverageClasses = [ '.cbranch-no', '.cstat-no', '.fstat-no' ];
|
||||
|
||||
// We don't want to select elements that are direct descendants of another match
|
||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||
|
||||
// Selecter that finds elements on the page to which we can jump
|
||||
var selector = notSelector + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||
|
||||
// The NodeList of matching elements
|
||||
var missingCoverageElements = document.querySelectorAll(selector);
|
||||
|
||||
var currentIndex;
|
||||
|
||||
function toggleClass(index) {
|
||||
missingCoverageElements.item(currentIndex).classList.remove('highlighted');
|
||||
missingCoverageElements.item(index).classList.add('highlighted');
|
||||
}
|
||||
|
||||
function makeCurrent(index) {
|
||||
toggleClass(index);
|
||||
currentIndex = index;
|
||||
missingCoverageElements.item(index)
|
||||
.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
var nextIndex = 0;
|
||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||
nextIndex = missingCoverageElements.length - 1;
|
||||
} else if (missingCoverageElements.length > 1) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
var nextIndex = 0;
|
||||
|
||||
if (typeof currentIndex === 'number' && currentIndex < (missingCoverageElements.length - 1)) {
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
return function jump(event) {
|
||||
switch (event.which) {
|
||||
case 78: // n
|
||||
case 74: // j
|
||||
goToNext();
|
||||
break;
|
||||
case 66: // b
|
||||
case 75: // k
|
||||
case 80: // p
|
||||
goToPrevious();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}());
|
||||
window.addEventListener('keydown', jumpToCode);
|
|
@ -0,0 +1,84 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>
|
||||
All files
|
||||
</h1>
|
||||
<div class='clearfix'>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div><div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage
|
||||
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Wed Jul 11 2018 10:21:48 GMT+0200 (heure d’été d’Europe centrale)
|
||||
</div>
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
if (typeof prettyPrint === 'function') {
|
||||
prettyPrint();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 209 B |
|
@ -0,0 +1,158 @@
|
|||
var addSorting = (function () {
|
||||
"use strict";
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() { return document.querySelector('.coverage-summary'); }
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() { return getTable().querySelector('thead tr'); }
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() { return getTable().querySelector('tbody'); }
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function (a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function (a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function () {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i =0 ; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function () {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData(cols);
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
|
@ -1,11 +0,0 @@
|
|||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(50, 50, 50, .4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 15px rgba(50, 50, 50, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(50, 50, 50, 0);
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
.contacts-list {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
contactlist .tooltip {
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.app-content-list-item-failed {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
margin-top: -15px;
|
||||
opacity: 0.2;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.app-content-list-item-failed:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.app-content-list-item-failed ~ .app-content-list-item-line-one,
|
||||
.app-content-list-item-failed ~ .app-content-list-item-line-two {
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.contact__icon {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
text-transform: capitalize;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.icon-group {
|
||||
background-image: url('../img/group.svg');
|
||||
}
|
||||
|
||||
/* Mobile width < 768px */
|
||||
@media only screen and (max-width: 768px) {
|
||||
.contacts-list:not(.mobile-show) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#app-content-wrapper {
|
||||
.app-content-list {
|
||||
display: block;
|
||||
}
|
||||
.app-content-detail {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#app-navigation-toggle.showdetails {
|
||||
transform: translate(-50px, 0);
|
||||
+ #app-content-wrapper {
|
||||
.app-content-list {
|
||||
display: none;
|
||||
}
|
||||
.app-content-detail {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#app-navigation-toggle-back {
|
||||
position: fixed;
|
||||
display: inline-block !important;
|
||||
top: 45px;
|
||||
left: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
z-index: 149;
|
||||
background-color: rgba(255, 255, 255, .7);
|
||||
cursor: pointer;
|
||||
opacity: .6;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* end of media query */
|
||||
}
|
|
@ -1,464 +0,0 @@
|
|||
.contact-details-wrapper {
|
||||
position: relative;
|
||||
background: $color-main-background;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.wrapper-show {
|
||||
z-index: 201;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.wrapper-show:not(.mobile-show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.contactdetails__header {
|
||||
height: 100px;
|
||||
padding-left: 44px;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contactdetails__header #details-contact-infos {
|
||||
width: 80%;
|
||||
margin: 2px 6px 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.contactdetails__header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.contactdetails__header #details-org-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contactdetails__header input[type=text]:active {
|
||||
background: transparent !important; /* remove :active effect */
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__name,
|
||||
.contactdetails__header .contactdetails__org,
|
||||
.contactdetails__header .contactdetails__title {
|
||||
font-size: inherit;
|
||||
/* Override focus, active & hover! */
|
||||
color: #fff !important; /* No vars used on purpose since we use custom BGs */
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, .2); // better readability on bright background colors
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__org,
|
||||
.contactdetails__header .contactdetails__title {
|
||||
max-width: 50%;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contactdetails__header #details-actions div.icon-more-white {
|
||||
cursor: pointer;
|
||||
padding: 14px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.contactdetails__header #contact-failed-save {
|
||||
animation: pulse 1.5s infinite;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* fix placeholder color */
|
||||
.contactdetails__header .contactdetails__name::-webkit-input-placeholder,
|
||||
.contactdetails__header .contactdetails__org::-webkit-input-placeholder,
|
||||
.contactdetails__header .contactdetails__title::-webkit-input-placeholder { /* WebKit, Blink, Edge */
|
||||
color: #fff; /* No vars used on purpose since we use custom BGs */
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__name::-moz-placeholder,
|
||||
.contactdetails__header .contactdetails__org::-moz-placeholder,
|
||||
.contactdetails__header .contactdetails__title::-moz-placeholder { /* Mozilla Firefox 19+ */
|
||||
color: #fff; /* No vars used on purpose since we use custom BGs */
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__name:-ms-input-placeholder,
|
||||
.contactdetails__header .contactdetails__org:-ms-input-placeholder,
|
||||
.contactdetails__header .contactdetails__title:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
color: #fff; /* No vars used on purpose since we use custom BGs */
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.contactdetails__header .contactdetails__name:placeholder-shown,
|
||||
.contactdetails__header .contactdetails__org:placeholder-shown,
|
||||
.contactdetails__header .contactdetails__title:placeholder-shown { /* Standard (https://drafts.csswg.org/selectors-4/#placeholder) */
|
||||
color: #fff; /* No vars used on purpose since we use custom BGs */
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
avatar {
|
||||
position: relative;
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contactdetails__logo {
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
object-fit: cover;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-options {
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-options > [class^='icon-'] {
|
||||
display: none;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.avatar-options:hover > [class^='icon-'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.avatar-options > [class^='icon-']:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
avatar.avatar--missing .avatar-options {
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
avatar:not(.maximized).avatar--missing .avatar-options .icon-upload-white {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
avatar.maximized {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 0;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
padding-top: 65px; /* Nextcloud header */
|
||||
}
|
||||
|
||||
avatar.maximized img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 0;
|
||||
max-height: calc(100% - 40px);
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
avatar.maximized .avatar-options {
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: calc(100% - 40px);
|
||||
left: 0;
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
avatar.maximized .avatar-options > [class^='icon-'] {
|
||||
min-width: 25%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.contactdetails__header + section {
|
||||
padding: 20px 20px 100px;
|
||||
}
|
||||
|
||||
/* GRID */
|
||||
$grid-height-unit: 40px;
|
||||
$grid-input-padding: 7px;
|
||||
$grid-input-margin: 3px;
|
||||
$grid-column-width: 380px;
|
||||
$grid-input-height-with-margin: #{$grid-height-unit - $grid-input-margin * 2};
|
||||
|
||||
@mixin generate-grid-span($default-unit) {
|
||||
/* we only supports 10 props of the same type */
|
||||
@for $i from 1 through 10 {
|
||||
&.grid-span-#{$i} {
|
||||
/* default unit + title + bottom padding */
|
||||
grid-row-start: span #{2 + $i * $default-unit};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
display: grid;
|
||||
/* unquote is a strange hack to avoid removal of the comma by the scss compiler */
|
||||
grid-template-columns: repeat(auto-fit, minmax(unquote('#{$grid-column-width}'), 1fr));
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
|
||||
/* General details item styles */
|
||||
contactdetails {
|
||||
select, button, input, textarea {
|
||||
&:disabled {
|
||||
background-color: transparent !important;
|
||||
border-color: transparent !important;
|
||||
opacity: 1 !important;
|
||||
color: #545454 !important;
|
||||
}
|
||||
}
|
||||
|
||||
detailsitem > select:disabled {
|
||||
background-image: none !important;
|
||||
&:first-child {
|
||||
opacity: 0.5 !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
propertygroup {
|
||||
display: block;
|
||||
&:not(.property-adr){
|
||||
/* adr detailsitem already have bottom padding */
|
||||
padding-bottom: $grid-height-unit;
|
||||
}
|
||||
.propertyGroup__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $grid-height-unit;
|
||||
padding: calc(#{$grid-height-unit} / 4);
|
||||
margin: 0;
|
||||
margin-left: 68px;
|
||||
opacity: .6;
|
||||
i {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
background-size: 16px 16px;
|
||||
margin-right: 8px;
|
||||
opacity: .5;
|
||||
}
|
||||
#info{
|
||||
cursor:pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
/* GRID SETTINGS */
|
||||
@include generate-grid-span(1);
|
||||
&.property-adr {
|
||||
@include generate-grid-span(8);
|
||||
}
|
||||
&.property-n {
|
||||
@include generate-grid-span(7);
|
||||
}
|
||||
&.property-note {
|
||||
@include generate-grid-span(5);
|
||||
}
|
||||
}
|
||||
|
||||
detailsitem input[type='tel'],
|
||||
detailsitem input[type='email'],
|
||||
detailsitem input[type='text'],
|
||||
detailsitem input[type='url'],
|
||||
detailsitem textarea,
|
||||
.select-addressbook select,
|
||||
.add-field {
|
||||
width: 245px;
|
||||
flex-grow: 1;
|
||||
margin: $grid-input-margin;
|
||||
height: $grid-input-height-with-margin;
|
||||
padding: $grid-input-padding;
|
||||
}
|
||||
|
||||
.add-field {
|
||||
margin-left: 106px;
|
||||
}
|
||||
|
||||
detailsitem label,
|
||||
.select-addressbook label {
|
||||
margin: $grid-input-margin;
|
||||
margin-left: 0;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: $grid-input-height-with-margin;
|
||||
padding: $grid-input-padding 0;
|
||||
text-align: right;
|
||||
opacity: .5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
detailsitem {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
width: $grid-column-width;
|
||||
> div {
|
||||
display: inline-flex;
|
||||
}
|
||||
select {
|
||||
width: 100px;
|
||||
height: $grid-input-height-with-margin;
|
||||
padding: $grid-input-padding;
|
||||
margin: $grid-input-margin;
|
||||
margin-left: 0;
|
||||
border: none;
|
||||
text-align: right;
|
||||
text-align-last: right;
|
||||
opacity: .5;
|
||||
color: $color-main-text;
|
||||
outline: none;
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& .icon-delete {
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
padding: 16px 10px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& input:hover + .icon-delete,
|
||||
& input:focus + .icon-delete,
|
||||
& input:active + .icon-delete,
|
||||
& select:hover + .icon-delete,
|
||||
& select:focus + .icon-delete,
|
||||
& select:active + .icon-delete,
|
||||
&:hover .icon-delete {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
& .icon-delete:hover,
|
||||
& .icon-delete:focus,
|
||||
& .icon-delete:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& .item-action {
|
||||
position: absolute;
|
||||
padding: 10px 5px;
|
||||
opacity: .5;
|
||||
right: 30px;
|
||||
~ input {
|
||||
padding-right: 30px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
i {
|
||||
display: block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.details-item-adr,
|
||||
&.details-item-n {
|
||||
padding-bottom: $grid-height-unit;
|
||||
.icon-delete {
|
||||
vertical-align: middle;
|
||||
left: 251px;
|
||||
}
|
||||
}
|
||||
|
||||
&.details-item-note label {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Failed props */
|
||||
&.failed {
|
||||
box-shadow: inset 2px 0 $color-error;
|
||||
}
|
||||
textarea {
|
||||
height: calc(#{$grid-input-height-with-margin} + (#{$grid-height-unit} * 4));
|
||||
}
|
||||
}
|
||||
|
||||
avatar .icon-error {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
avatar:not(.maximized) .icon-error + img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Prevent delete for last adr/mail/tel item */
|
||||
.last-details > detailsitem.details-item-adr .icon-delete,
|
||||
.last-details > detailsitem.details-item-email .icon-delete,
|
||||
.last-details > detailsitem.details-item-tel .icon-delete {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* SELECT2 styling */
|
||||
detailsitem .select2-container {
|
||||
width: 245px;
|
||||
}
|
||||
|
||||
/* Fix for #81 */
|
||||
.select2-container-multi .select2-choices .select2-search-choice {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.select2-container.select2-container-multi .select2-choices span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close {
|
||||
display: block !important;
|
||||
right: 4px;
|
||||
left: auto;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
/* Fix disabled select2 state */
|
||||
detailsitem .select2-container[disabled] .select2-choices {
|
||||
border-color: transparent;
|
||||
min-height: 100%;
|
||||
background-color: transparent !important;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
|
||||
#app-navigation > ul {
|
||||
height: calc(100% - 68px);
|
||||
}
|
||||
|
||||
#app-navigation .app-navigation-entry-utils .app-navigation-entry-utils-counter {
|
||||
padding: 0 12px 0 0;
|
||||
}
|
||||
|
||||
/* Contacts List */
|
||||
#new-contact-button {
|
||||
margin: 14px auto; /* to have the same height than a contact */
|
||||
width: calc(100% - 20px) !important;
|
||||
text-align: left;
|
||||
background-position: 10px center;
|
||||
padding: 10px;
|
||||
padding-left: 34px;
|
||||
display: block;
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
.settings-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.addressBookList form {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.addressBookList form input.ng-invalid-pattern {
|
||||
border-color: $color-error !important;
|
||||
}
|
||||
|
||||
ul.addressBookList > li {
|
||||
padding: 6px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
ul.addressBookList > li.newAddressBookContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul.addressBookList > li.newAddressBookContainer .tooltip {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
ul.addressBook-share-list {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
li {
|
||||
padding: 0 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
opacity: .2;
|
||||
}
|
||||
.utils {
|
||||
display: flex;
|
||||
.checkbox + label {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* override core apps css */
|
||||
#app-navigation ul.addressBookList > li span.utils {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
ul.addressBookList li .utils .popovermenu {
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
#app-navigation ul.addressBookList li .utils .popovermenu li > button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul.addressBookList li .action {
|
||||
opacity: 0.3;
|
||||
display: block;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
ul.addressBookList li .action > span {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
ul.addressBookList li .action > a {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
addressBookList input[type='submit'].inline-button,
|
||||
addressBookList input[type='button'].inline-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 6px 15px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
opacity: .5;
|
||||
margin-right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul.addressBookList li[addressbook] > span.addressBookName {
|
||||
width: calc(100% - 52px); /* -actions width */
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 7px;
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
ul.addressBookList li[addressbook] > .addressBookShares {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
li.addressBook-share-item span.shareeIdentifier,
|
||||
li.calendar-share-item span.shareeIdentifier {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
div.addressBookShares ul.dropdown-menu {
|
||||
border: 1px solid nc-darken($color-main-background, 18%);
|
||||
border-radius: 0 0 3px 3px;
|
||||
max-height: 200px;
|
||||
margin-top: -2px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: absolute;
|
||||
background-color: $color-main-background;
|
||||
width: 100%;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
div.addressBookShares ul.dropdown-menu li > a {
|
||||
height: 30px !important;
|
||||
min-height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
|
||||
ul.dropdown-menu li {
|
||||
width: 100%;
|
||||
padding: 3px 7px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul.dropdown-menu li.active {
|
||||
background: nc-darken($color-main-background, 6%);
|
||||
}
|
||||
|
||||
div.app-contacts span.utils {
|
||||
padding: 0 !important;
|
||||
float: right;
|
||||
position: relative !important;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.addressBookUrlContainer {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input.renameAddressBookInput,
|
||||
input.newAddressBookInput,
|
||||
input.shareeInput,
|
||||
input.addressBookUrl {
|
||||
width: 100% !important;
|
||||
margin-right: 0;
|
||||
padding-right: 30px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select2-drop .select2-search input {
|
||||
width: 100% !important;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Contact import */
|
||||
#app-settings-content #upload.button {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
padding-left: 34px;
|
||||
background-position: 10px center;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
#app-settings-content #upload.button::after {
|
||||
left: 17px; /* half the padding */
|
||||
}
|
||||
|
||||
#app-settings-content #upload.button.no-select {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
contactimport {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
contactimport .select2-container {
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
contactimport .select2-container::after {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
contactimport .select2-container .select2-choice {
|
||||
height: 100%;
|
||||
line-height: 31px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
contactimport .select2-drop-active {
|
||||
border-top: 1px solid nc-darken($color-main-background, 18%);
|
||||
box-shadow: 0 -1px 5px rgba($color-main-background, .15);
|
||||
border-radius: 3px 3px 0 0;
|
||||
margin-top: initial;
|
||||
}
|
||||
|
||||
contactimport .ui-select-offscreen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
contactimport .ui-select-search-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
contactimport input[type='search']::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* Import screen */
|
||||
#import-sidebar {
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
z-index: 500;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#importscreen-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
background: $color-main-background;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
#importscreen-content {
|
||||
width: 300px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#importscreen-title {
|
||||
flex-basis: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#importscreen-percent {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#importscreen-user {
|
||||
opacity: 0.5;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
/* Copy nextcloud quota bar */
|
||||
#importscreen-progress {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0 none;
|
||||
background-color: nc-darken($color-main-background, 10%);
|
||||
border-radius: 3px;
|
||||
flex-basis: 100%;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#importscreen-progress::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#importscreen-progress::-moz-progress-bar {
|
||||
border-radius: 3px;
|
||||
background: $color-primary;
|
||||
transition: 500ms all ease-in-out;
|
||||
}
|
||||
|
||||
#importscreen-progress::-webkit-progress-value {
|
||||
border-radius: 3px;
|
||||
background: $color-primary;
|
||||
transition: 500ms all ease-in-out;
|
||||
}
|
||||
|
||||
#importscreen-sidebar-block {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $color-main-background;
|
||||
z-index: 500;
|
||||
opacity: 0.5;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
@import 'details';
|
||||
@import 'contactlist';
|
||||
@import 'navigation';
|
||||
@import 'settings';
|
||||
@import 'animations';
|
85
gulpfile.js
85
gulpfile.js
|
@ -1,85 +0,0 @@
|
|||
var gulp = require('gulp'),
|
||||
concat = require('gulp-concat'),
|
||||
eslint = require('gulp-eslint'),
|
||||
stylelint = require('gulp-stylelint');
|
||||
ngAnnotate = require('gulp-ng-annotate'),
|
||||
merge = require('merge-stream'),
|
||||
KarmaServer = require('karma').Server,
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
var dependencies = require('./vendorScripts.json');
|
||||
|
||||
gulp.task('build', function() {
|
||||
return gulp.src([
|
||||
'js/main.js',
|
||||
'js/components/**/*.js',
|
||||
'js/models/**/*.js',
|
||||
'js/services/**/*.js',
|
||||
'js/filters/**/*.js'
|
||||
])
|
||||
// concat (+sourcemaps)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(ngAnnotate({ single_quotes: true }))
|
||||
.pipe(concat('script.js'))
|
||||
.pipe(sourcemaps.write())
|
||||
|
||||
.pipe(gulp.dest('js/public'));
|
||||
});
|
||||
|
||||
gulp.task('vendor', function() {
|
||||
let stream = require('merge-stream')();;
|
||||
|
||||
for(let dependency in dependencies.scripts) {
|
||||
stream.add(
|
||||
gulp.src(dependencies.scripts[dependency])
|
||||
.pipe(gulp.dest(`js/vendor/${dependency}`))
|
||||
);
|
||||
}
|
||||
|
||||
for(let dependency in dependencies.styles) {
|
||||
stream.add(
|
||||
gulp.src(dependencies.styles[dependency])
|
||||
.pipe(gulp.dest(`css/vendor/${dependency}`))
|
||||
);
|
||||
}
|
||||
|
||||
return stream;
|
||||
});
|
||||
|
||||
gulp.task('eslint', function() {
|
||||
return gulp.src([
|
||||
'js/main.js',
|
||||
'js/components/**/*.js',
|
||||
'js/models/**/*.js',
|
||||
'js/services/**/*.js',
|
||||
'js/filters/**/*.js'
|
||||
])
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failAfterError());
|
||||
});
|
||||
|
||||
gulp.task('stylelint', function() {
|
||||
return gulp.src('css/*.scss')
|
||||
.pipe(stylelint({
|
||||
reporters: [
|
||||
{formatter: 'string', console: true}
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
gulp.task('karma', function(done){
|
||||
new KarmaServer({
|
||||
configFile: __dirname + '/karma.conf.js',
|
||||
singleRun: true
|
||||
}, done).start();
|
||||
});
|
||||
|
||||
|
||||
gulp.task('default', ['vendor', 'eslint', 'stylelint', 'build']);
|
||||
|
||||
gulp.task('test', ['karma']);
|
||||
|
||||
gulp.task('watch', ['default'], function() {
|
||||
gulp.watch(['js/**/*.js', '!js/public/**/*.js', 'css/*.scss'], ['eslint', 'stylelint', 'build']);
|
||||
});
|
|
@ -1,202 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('addressbookCtrl', function($scope, AddressBookService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.t = {
|
||||
download: t('contacts', 'Download'),
|
||||
copyURL: t('contacts', 'Copy link'),
|
||||
clickToCopy: t('contacts', 'Click to copy the link to your clipboard'),
|
||||
shareAddressbook: t('contacts', 'Toggle sharing'),
|
||||
deleteAddressbook: t('contacts', 'Delete'),
|
||||
renameAddressbook: t('contacts', 'Rename'),
|
||||
shareInputPlaceHolder: t('contacts', 'Share with users or groups'),
|
||||
delete: t('contacts', 'Delete'),
|
||||
canEdit: t('contacts', 'can edit'),
|
||||
close: t('contacts', 'Close'),
|
||||
enabled: t('contacts', 'Enabled'),
|
||||
disabled: t('contacts', 'Disabled')
|
||||
};
|
||||
|
||||
ctrl.editing = false;
|
||||
ctrl.enabled = ctrl.addressBook.enabled;
|
||||
|
||||
ctrl.tooltipIsOpen = false;
|
||||
ctrl.tooltipTitle = ctrl.t.clickToCopy;
|
||||
ctrl.showInputUrl = false;
|
||||
|
||||
ctrl.clipboardSuccess = function() {
|
||||
ctrl.tooltipIsOpen = true;
|
||||
ctrl.tooltipTitle = t('core', 'Copied!');
|
||||
_.delay(function() {
|
||||
ctrl.tooltipIsOpen = false;
|
||||
ctrl.tooltipTitle = ctrl.t.clickToCopy;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
ctrl.clipboardError = function() {
|
||||
ctrl.showInputUrl = true;
|
||||
if (/iPhone|iPad/i.test(navigator.userAgent)) {
|
||||
ctrl.InputUrlTooltip = t('core', 'Not supported!');
|
||||
} else if (/Mac/i.test(navigator.userAgent)) {
|
||||
ctrl.InputUrlTooltip = t('core', 'Press ⌘-C to copy.');
|
||||
} else {
|
||||
ctrl.InputUrlTooltip = t('core', 'Press Ctrl-C to copy.');
|
||||
}
|
||||
$('#addressBookUrl_'+ctrl.addressBook.ctag).select();
|
||||
};
|
||||
|
||||
ctrl.renameAddressBook = function() {
|
||||
AddressBookService.rename(ctrl.addressBook, ctrl.addressBook.displayName);
|
||||
ctrl.editing = false;
|
||||
};
|
||||
|
||||
ctrl.edit = function() {
|
||||
ctrl.editing = true;
|
||||
};
|
||||
|
||||
ctrl.closeMenus = function() {
|
||||
$scope.$parent.ctrl.openedMenu = false;
|
||||
};
|
||||
|
||||
ctrl.openMenu = function(index) {
|
||||
ctrl.closeMenus();
|
||||
$scope.$parent.ctrl.openedMenu = index;
|
||||
};
|
||||
|
||||
ctrl.toggleMenu = function(index) {
|
||||
if ($scope.$parent.ctrl.openedMenu === index) {
|
||||
ctrl.closeMenus();
|
||||
} else {
|
||||
ctrl.openMenu(index);
|
||||
}
|
||||
};
|
||||
|
||||
ctrl.toggleSharesEditor = function() {
|
||||
ctrl.editingShares = !ctrl.editingShares;
|
||||
ctrl.selectedSharee = null;
|
||||
};
|
||||
|
||||
/* From Calendar-Rework - js/app/controllers/calendarlistcontroller.js */
|
||||
ctrl.findSharee = function (val) {
|
||||
return $.get(
|
||||
OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
|
||||
{
|
||||
format: 'json',
|
||||
search: val.trim(),
|
||||
perPage: 200,
|
||||
itemType: 'principals'
|
||||
}
|
||||
).then(function(result) {
|
||||
var users = result.ocs.data.exact.users.concat(result.ocs.data.users);
|
||||
var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups);
|
||||
|
||||
var userShares = ctrl.addressBook.sharedWith.users;
|
||||
var userSharesLength = userShares.length;
|
||||
|
||||
var groupsShares = ctrl.addressBook.sharedWith.groups;
|
||||
var groupsSharesLength = groupsShares.length;
|
||||
var i, j;
|
||||
|
||||
// Filter out current user
|
||||
for (i = 0 ; i < users.length; i++) {
|
||||
if (users[i].value.shareWith === OC.currentUser) {
|
||||
users.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now filter out all sharees that are already shared with
|
||||
for (i = 0; i < userSharesLength; i++) {
|
||||
var shareUser = userShares[i];
|
||||
for (j = 0; j < users.length; j++) {
|
||||
if (users[j].value.shareWith === shareUser.id) {
|
||||
users.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now filter out all groups that are already shared with
|
||||
for (i = 0; i < groupsSharesLength; i++) {
|
||||
var sharedGroup = groupsShares[i];
|
||||
for (j = 0; j < groups.length; j++) {
|
||||
if (groups[j].value.shareWith === sharedGroup.id) {
|
||||
groups.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine users and groups
|
||||
users = users.map(function(item) {
|
||||
return {
|
||||
display: _.escape(item.value.shareWith),
|
||||
type: OC.Share.SHARE_TYPE_USER,
|
||||
identifier: item.value.shareWith
|
||||
};
|
||||
});
|
||||
|
||||
groups = groups.map(function(item) {
|
||||
return {
|
||||
display: _.escape(item.value.shareWith) + ' (group)',
|
||||
type: OC.Share.SHARE_TYPE_GROUP,
|
||||
identifier: item.value.shareWith
|
||||
};
|
||||
});
|
||||
|
||||
return groups.concat(users);
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.onSelectSharee = function (item) {
|
||||
// Prevent settings to slide down
|
||||
$('#app-settings-header > button').data('apps-slide-toggle', false);
|
||||
_.delay(function() {
|
||||
$('#app-settings-header > button').data('apps-slide-toggle', '#app-settings-content');
|
||||
}, 500);
|
||||
|
||||
ctrl.selectedSharee = null;
|
||||
AddressBookService.share(ctrl.addressBook, item.type, item.identifier, false, false).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
ctrl.updateExistingUserShare = function(userId, writable) {
|
||||
AddressBookService.share(ctrl.addressBook, OC.Share.SHARE_TYPE_USER, userId, writable, true).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.updateExistingGroupShare = function(groupId, writable) {
|
||||
AddressBookService.share(ctrl.addressBook, OC.Share.SHARE_TYPE_GROUP, groupId, writable, true).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.unshareFromUser = function(userId) {
|
||||
AddressBookService.unshare(ctrl.addressBook, OC.Share.SHARE_TYPE_USER, userId).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.unshareFromGroup = function(groupId) {
|
||||
AddressBookService.unshare(ctrl.addressBook, OC.Share.SHARE_TYPE_GROUP, groupId).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.deleteAddressBook = function() {
|
||||
AddressBookService.delete(ctrl.addressBook).then(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.toggleState = function() {
|
||||
AddressBookService.toggleState(ctrl.addressBook).then(function(addressBook) {
|
||||
ctrl.enabled = addressBook.enabled;
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('addressbook', function() {
|
||||
return {
|
||||
restrict: 'A', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'addressbookCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
addressBook: '=data',
|
||||
list: '='
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/addressBook.html')
|
||||
};
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('addressbooklistCtrl', function($scope, AddressBookService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.loading = true;
|
||||
ctrl.openedMenu = false;
|
||||
ctrl.addressBookRegex = /^[a-zA-Z0-9À-ÿ\s-_.!?#|()]+$/i;
|
||||
|
||||
AddressBookService.getAll().then(function(addressBooks) {
|
||||
ctrl.addressBooks = addressBooks;
|
||||
ctrl.loading = false;
|
||||
if(ctrl.addressBooks.length === 0) {
|
||||
AddressBookService.create(t('contacts', 'Contacts')).then(function() {
|
||||
AddressBookService.getAddressBook(t('contacts', 'Contacts')).then(function(addressBook) {
|
||||
ctrl.addressBooks.push(addressBook);
|
||||
$scope.$apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.t = {
|
||||
addressBookName : t('contacts', 'Address book name'),
|
||||
regexError : t('contacts', 'Only these special characters are allowed: -_.!?#|()')
|
||||
};
|
||||
|
||||
ctrl.createAddressBook = function() {
|
||||
if(ctrl.newAddressBookName) {
|
||||
AddressBookService.create(ctrl.newAddressBookName).then(function() {
|
||||
AddressBookService.getAddressBook(ctrl.newAddressBookName).then(function(addressBook) {
|
||||
ctrl.addressBooks.push(addressBook);
|
||||
$scope.$apply();
|
||||
});
|
||||
}).catch(function() {
|
||||
OC.Notification.showTemporary(t('contacts', 'Address book could not be created.'));
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('addressbooklist', function() {
|
||||
return {
|
||||
restrict: 'EA', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'addressbooklistCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/addressBookList.html')
|
||||
};
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('avatarCtrl', function(ContactService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.import = ContactService.import.bind(ContactService);
|
||||
|
||||
ctrl.removePhoto = function() {
|
||||
ctrl.contact.removeProperty('photo', ctrl.contact.getProperty('photo'));
|
||||
ContactService.update(ctrl.contact);
|
||||
$('avatar').removeClass('maximized');
|
||||
};
|
||||
|
||||
ctrl.downloadPhoto = function() {
|
||||
/* globals ArrayBuffer, Uint8Array */
|
||||
var img = document.getElementById('contact-avatar');
|
||||
// atob to base64_decode the data-URI
|
||||
var imageSplit = img.src.split(',');
|
||||
// "data:image/png;base64" -> "png"
|
||||
var extension = '.' + imageSplit[0].split(';')[0].split('/')[1];
|
||||
var imageData = atob(imageSplit[1]);
|
||||
// Use typed arrays to convert the binary data to a Blob
|
||||
var arrayBuffer = new ArrayBuffer(imageData.length);
|
||||
var view = new Uint8Array(arrayBuffer);
|
||||
for (var i=0; i<imageData.length; i++) {
|
||||
view[i] = imageData.charCodeAt(i) & 0xff;
|
||||
}
|
||||
var blob = new Blob([arrayBuffer], {type: 'application/octet-stream'});
|
||||
|
||||
// Use the URL object to create a temporary URL
|
||||
var url = (window.webkitURL || window.URL).createObjectURL(blob);
|
||||
|
||||
var a = document.createElement('a');
|
||||
document.body.appendChild(a);
|
||||
a.style = 'display: none';
|
||||
a.href = url;
|
||||
a.download = ctrl.contact.uid() + extension;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
};
|
||||
|
||||
ctrl.openPhoto = function() {
|
||||
$('avatar').toggleClass('maximized');
|
||||
};
|
||||
|
||||
ctrl.t = {
|
||||
uploadNewPhoto : t('contacts', 'Upload new image'),
|
||||
deletePhoto : t('contacts', 'Delete'),
|
||||
closePhoto : t('contacts', 'Close'),
|
||||
downloadPhoto : t('contacts', 'Download')
|
||||
};
|
||||
|
||||
// Quit avatar preview
|
||||
$('avatar').click(function() {
|
||||
$('avatar').removeClass('maximized');
|
||||
});
|
||||
$('avatar img, avatar .avatar-options').click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$(document).keyup(function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
$('avatar').removeClass('maximized');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('avatar', function(ContactService) {
|
||||
return {
|
||||
scope: {
|
||||
contact: '=data'
|
||||
},
|
||||
controller: 'avatarCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
contact: '=data'
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var input = element.find('input');
|
||||
input.bind('change', function() {
|
||||
var file = input.get(0).files[0];
|
||||
if (file.size > 1024*1024) { // 1 MB
|
||||
OC.Notification.showTemporary(t('contacts', 'The selected image is too big (max 1MB)'));
|
||||
} else {
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', function () {
|
||||
scope.$apply(function() {
|
||||
scope.contact.photo(reader.result);
|
||||
ContactService.update(scope.contact);
|
||||
});
|
||||
}, false);
|
||||
|
||||
if (file) {
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/avatar.html')
|
||||
};
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('contactCtrl', function($route, $routeParams, SortByService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.t = {
|
||||
errorMessage : t('contacts', 'This card is corrupted and has been fixed. Please check the data and trigger a save to make the changes permanent.'),
|
||||
};
|
||||
|
||||
ctrl.getName = function() {
|
||||
// If lastName equals to firstName then none of them is set
|
||||
if (ctrl.contact.lastName() === ctrl.contact.firstName()) {
|
||||
return ctrl.contact.displayName();
|
||||
}
|
||||
|
||||
if (SortByService.getSortByKey() === 'sortLastName') {
|
||||
return (
|
||||
ctrl.contact.lastName()
|
||||
+ (ctrl.contact.firstName() ? ', ' : '')
|
||||
+ ctrl.contact.firstName() + ' '
|
||||
+ ctrl.contact.additionalNames()
|
||||
).trim();
|
||||
}
|
||||
|
||||
if (SortByService.getSortByKey() === 'sortFirstName') {
|
||||
return (
|
||||
ctrl.contact.firstName() + ' '
|
||||
+ ctrl.contact.additionalNames() + ' '
|
||||
+ ctrl.contact.lastName()
|
||||
).trim();
|
||||
}
|
||||
|
||||
return ctrl.contact.displayName();
|
||||
};
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('contact', function() {
|
||||
return {
|
||||
scope: {},
|
||||
controller: 'contactCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
contact: '=data'
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/contact.html')
|
||||
};
|
||||
});
|
|
@ -1,118 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('contactdetailsCtrl', function(ContactService, AddressBookService, vCardPropertiesService, $route, $routeParams, $scope) {
|
||||
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.init = true;
|
||||
ctrl.loading = false;
|
||||
ctrl.show = false;
|
||||
|
||||
ctrl.clearContact = function() {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: undefined
|
||||
});
|
||||
ctrl.show = false;
|
||||
ctrl.contact = undefined;
|
||||
};
|
||||
|
||||
ctrl.uid = $routeParams.uid;
|
||||
ctrl.t = {
|
||||
noContacts : t('contacts', 'No contacts in here'),
|
||||
placeholderName : t('contacts', 'Name'),
|
||||
placeholderOrg : t('contacts', 'Organization'),
|
||||
placeholderTitle : t('contacts', 'Title'),
|
||||
selectField : t('contacts', 'Add field …'),
|
||||
download : t('contacts', 'Download'),
|
||||
delete : t('contacts', 'Delete'),
|
||||
save : t('contacts', 'Save changes'),
|
||||
addressBook : t('contacts', 'Address book'),
|
||||
loading : t('contacts', 'Loading contacts …')
|
||||
};
|
||||
|
||||
ctrl.fieldDefinitions = vCardPropertiesService.fieldDefinitions;
|
||||
ctrl.focus = undefined;
|
||||
ctrl.field = undefined;
|
||||
ctrl.addressBooks = [];
|
||||
|
||||
AddressBookService.getAll().then(function(addressBooks) {
|
||||
ctrl.addressBooks = addressBooks;
|
||||
|
||||
if (!angular.isUndefined(ctrl.contact)) {
|
||||
ctrl.addressBook = _.find(ctrl.addressBooks, function(book) {
|
||||
return book.displayName === ctrl.contact.addressBookId;
|
||||
});
|
||||
}
|
||||
ctrl.init = false;
|
||||
// Start watching for ctrl.uid when we have addressBooks, as they are needed for fetching
|
||||
// full details.
|
||||
$scope.$watch('ctrl.uid', function(newValue) {
|
||||
ctrl.changeContact(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
ctrl.changeContact = function(uid) {
|
||||
if (typeof uid === 'undefined') {
|
||||
ctrl.show = false;
|
||||
$('#app-navigation-toggle').removeClass('showdetails');
|
||||
return;
|
||||
}
|
||||
ctrl.loading = true;
|
||||
ContactService.getById(ctrl.addressBooks, uid).then(function(contact) {
|
||||
if (angular.isUndefined(contact)) {
|
||||
ctrl.clearContact();
|
||||
return;
|
||||
}
|
||||
ctrl.contact = contact;
|
||||
ctrl.show = true;
|
||||
ctrl.loading = false;
|
||||
$('#app-navigation-toggle').addClass('showdetails');
|
||||
|
||||
ctrl.addressBook = _.find(ctrl.addressBooks, function(book) {
|
||||
return book.displayName === ctrl.contact.addressBookId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.deleteContact = function() {
|
||||
ContactService.delete(ctrl.addressBook, ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.addField = function(field) {
|
||||
var defaultValue = vCardPropertiesService.getMeta(field).defaultValue || {value: ''};
|
||||
ctrl.contact.addProperty(field, defaultValue);
|
||||
ctrl.focus = field;
|
||||
ctrl.field = '';
|
||||
};
|
||||
|
||||
ctrl.deleteField = function (field, prop) {
|
||||
ctrl.contact.removeProperty(field, prop);
|
||||
ctrl.focus = undefined;
|
||||
};
|
||||
|
||||
ctrl.changeAddressBook = function (addressBook, oldAddressBook) {
|
||||
ContactService.moveContact(ctrl.contact, addressBook, oldAddressBook);
|
||||
};
|
||||
|
||||
ctrl.updateContact = function() {
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.closeMenus = function() {
|
||||
ctrl.openedMenu = false;
|
||||
};
|
||||
|
||||
ctrl.openMenu = function(index) {
|
||||
ctrl.closeMenus();
|
||||
ctrl.openedMenu = index;
|
||||
};
|
||||
|
||||
ctrl.toggleMenu = function(index) {
|
||||
if (ctrl.openedMenu === index) {
|
||||
ctrl.closeMenus();
|
||||
} else {
|
||||
ctrl.openMenu(index);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('contactdetails', function() {
|
||||
return {
|
||||
priority: 1,
|
||||
scope: {},
|
||||
controller: 'contactdetailsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/contactDetails.html')
|
||||
};
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('contactfilterCtrl', function() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var ctrl = this;
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('contactFilter', function() {
|
||||
return {
|
||||
restrict: 'A', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'contactfilterCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
contactFilter: '=contactFilter'
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/contactFilter.html')
|
||||
};
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('contactimportCtrl', function(ContactService, AddressBookService, $timeout, $scope) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.t = {
|
||||
importText : t('contacts', 'Import into'),
|
||||
importingText : t('contacts', 'Importing...'),
|
||||
selectAddressbook : t('contacts', 'Select your addressbook'),
|
||||
importdisabled : t('contacts', 'Import is disabled because no writable address book had been found.')
|
||||
};
|
||||
|
||||
ctrl.import = ContactService.import.bind(ContactService);
|
||||
ctrl.loading = true;
|
||||
ctrl.importText = ctrl.t.importText;
|
||||
ctrl.importing = false;
|
||||
ctrl.loadingClass = 'icon-upload';
|
||||
|
||||
AddressBookService.getAll().then(function(addressBooks) {
|
||||
ctrl.addressBooks = addressBooks;
|
||||
ctrl.loading = false;
|
||||
ctrl.selectedAddressBook = AddressBookService.getDefaultAddressBook();
|
||||
});
|
||||
|
||||
AddressBookService.registerObserverCallback(function() {
|
||||
$timeout(function() {
|
||||
$scope.$apply(function() {
|
||||
ctrl.selectedAddressBook = AddressBookService.getDefaultAddressBook();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ctrl.stopHideMenu = function(isOpen) {
|
||||
if(isOpen) {
|
||||
// disabling settings bind
|
||||
$('#app-settings-header > button').data('apps-slide-toggle', false);
|
||||
} else {
|
||||
// reenabling it
|
||||
$('#app-settings-header > button').data('apps-slide-toggle', '#app-settings-content');
|
||||
}
|
||||
};
|
||||
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('contactimport', function(ContactService, ImportService, $rootScope) {
|
||||
return {
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var input = element.find('input');
|
||||
input.bind('change', function() {
|
||||
angular.forEach(input.get(0).files, function(file) {
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', function () {
|
||||
scope.$apply(function () {
|
||||
// Indicate the user we started something
|
||||
ctrl.importText = ctrl.t.importingText;
|
||||
ctrl.loadingClass = 'icon-loading-small';
|
||||
ctrl.importing = true;
|
||||
$rootScope.importing = true;
|
||||
|
||||
ContactService.import.call(ContactService, reader.result, file.type, ctrl.selectedAddressBook, function (progress, user) {
|
||||
if (progress === 1) {
|
||||
ctrl.importText = ctrl.t.importText;
|
||||
ctrl.loadingClass = 'icon-upload';
|
||||
ctrl.importing = false;
|
||||
$rootScope.importing = false;
|
||||
ImportService.importPercent = 0;
|
||||
ImportService.importing = false;
|
||||
ImportService.importedUser = '';
|
||||
ImportService.selectedAddressBook = '';
|
||||
} else {
|
||||
// Ugly hack, hide sidebar on import & mobile
|
||||
// Simulate click since we can't directly access snapper
|
||||
if($(window).width() <= 768 && $('body').hasClass('snapjs-left')) {
|
||||
$('#app-navigation-toggle').click();
|
||||
$('body').removeClass('snapjs-left');
|
||||
}
|
||||
|
||||
ImportService.importPercent = parseInt(Math.floor(progress * 100));
|
||||
ImportService.importing = true;
|
||||
ImportService.importedUser = user;
|
||||
ImportService.selectedAddressBook = ctrl.selectedAddressBook.displayName;
|
||||
}
|
||||
scope.$apply();
|
||||
|
||||
/* Broadcast service update */
|
||||
$rootScope.$broadcast('importing', true);
|
||||
});
|
||||
});
|
||||
}, false);
|
||||
|
||||
if (file) {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
input.get(0).value = '';
|
||||
});
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/contactImport.html'),
|
||||
controller: 'contactimportCtrl',
|
||||
controllerAs: 'ctrl'
|
||||
};
|
||||
});
|
|
@ -1,283 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('contactlistCtrl', function($scope, $filter, $route, $routeParams, $timeout, AddressBookService, ContactService, SortByService, vCardPropertiesService, SearchService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.routeParams = $routeParams;
|
||||
|
||||
ctrl.filteredContacts = []; // the displayed contacts list
|
||||
ctrl.searchTerm = '';
|
||||
ctrl.show = true;
|
||||
ctrl.invalid = false;
|
||||
ctrl.limitTo = 25;
|
||||
|
||||
ctrl.sortBy = SortByService.getSortBy();
|
||||
|
||||
ctrl.t = {
|
||||
emptySearch : t('contacts', 'No search result for {query}', {query: ctrl.searchTerm})
|
||||
};
|
||||
|
||||
ctrl.resetLimitTo = function () {
|
||||
ctrl.limitTo = 25;
|
||||
clearInterval(ctrl.intervalId);
|
||||
ctrl.intervalId = setInterval(
|
||||
function () {
|
||||
if (!ctrl.loading && ctrl.contactList && ctrl.contactList.length > ctrl.limitTo) {
|
||||
ctrl.limitTo += 25;
|
||||
$scope.$apply();
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.query = function(contact) {
|
||||
return contact.matches(SearchService.getSearchTerm());
|
||||
};
|
||||
|
||||
SortByService.subscribe(function(newValue) {
|
||||
ctrl.sortBy = newValue;
|
||||
});
|
||||
|
||||
SearchService.registerObserverCallback(function(ev) {
|
||||
if (ev.event === 'submitSearch') {
|
||||
var uid = !_.isEmpty(ctrl.filteredContacts) ? ctrl.filteredContacts[0].uid() : undefined;
|
||||
ctrl.setSelectedId(uid);
|
||||
$scope.$apply();
|
||||
}
|
||||
if (ev.event === 'changeSearch') {
|
||||
ctrl.resetLimitTo();
|
||||
ctrl.searchTerm = ev.searchTerm;
|
||||
ctrl.t.emptySearch = t('contacts',
|
||||
'No search result for {query}',
|
||||
{query: ctrl.searchTerm}
|
||||
);
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.loading = true;
|
||||
|
||||
ContactService.registerObserverCallback(function(ev) {
|
||||
/* after import at first refresh the contactList */
|
||||
if (ev.event === 'importend') {
|
||||
$scope.$apply(function() {
|
||||
ctrl.contactList = ev.contacts;
|
||||
});
|
||||
}
|
||||
/* update route parameters */
|
||||
$timeout(function() {
|
||||
$scope.$apply(function() {
|
||||
switch(ev.event) {
|
||||
case 'delete':
|
||||
ctrl.selectNearestContact(ev.uid);
|
||||
break;
|
||||
case 'create':
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: ev.uid
|
||||
});
|
||||
break;
|
||||
case 'importend':
|
||||
/* after import select 'All contacts' group and first contact */
|
||||
$route.updateParams({
|
||||
gid: t('contacts', 'All contacts'),
|
||||
uid: ctrl.filteredContacts.length !== 0 ? ctrl.filteredContacts[0].uid() : undefined
|
||||
});
|
||||
return;
|
||||
case 'getFullContacts' || 'update':
|
||||
break;
|
||||
default:
|
||||
// unknown event -> leave callback without action
|
||||
return;
|
||||
}
|
||||
ctrl.contactList = ev.contacts;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
AddressBookService.registerObserverCallback(function(ev) {
|
||||
$timeout(function() {
|
||||
$scope.$apply(function() {
|
||||
switch (ev.event) {
|
||||
case 'delete':
|
||||
case 'disable':
|
||||
ctrl.loading = true;
|
||||
ContactService.removeContactsFromAddressbook(ev.addressBook, function() {
|
||||
ContactService.getAll().then(function(contacts) {
|
||||
ctrl.contactList = contacts;
|
||||
ctrl.loading = false;
|
||||
// Only change contact if the selectd one is not in the list anymore
|
||||
if(ctrl.contactList.findIndex(function(contact) {
|
||||
return contact.uid() === ctrl.getSelectedId();
|
||||
}) === -1) {
|
||||
ctrl.selectNearestContact(ctrl.getSelectedId());
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'enable':
|
||||
ctrl.loading = true;
|
||||
ContactService.appendContactsFromAddressbook(ev.addressBook, function() {
|
||||
ContactService.getAll().then(function(contacts) {
|
||||
ctrl.contactList = contacts;
|
||||
ctrl.loading = false;
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// unknown event -> leave callback without action
|
||||
return;
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Get contacts
|
||||
ContactService.getAll().then(function(contacts) {
|
||||
if(contacts.length>0) {
|
||||
$scope.$apply(function() {
|
||||
ctrl.contactList = contacts;
|
||||
});
|
||||
} else {
|
||||
ctrl.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
var getVisibleContacts = function() {
|
||||
var scrolled = $('.app-content-list').scrollTop();
|
||||
var elHeight = $('.contacts-list').children().outerHeight(true);
|
||||
var listHeight = $('.app-content-list').height();
|
||||
|
||||
var topContact = Math.round(scrolled/elHeight);
|
||||
var contactsCount = Math.round(listHeight/elHeight);
|
||||
|
||||
return ctrl.filteredContacts.slice(topContact-1, topContact+contactsCount+1);
|
||||
};
|
||||
|
||||
var timeoutId = null;
|
||||
document.querySelector('.app-content-list').addEventListener('scroll', function () {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(function () {
|
||||
var contacts = getVisibleContacts();
|
||||
ContactService.getFullContacts(contacts);
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Wait for ctrl.filteredContacts to be updated, load the contact requested in the URL if any, and
|
||||
// load full details for the probably initially visible contacts.
|
||||
// Then kill the watch.
|
||||
var unbindListWatch = $scope.$watch('ctrl.filteredContacts', function() {
|
||||
if(ctrl.filteredContacts && ctrl.filteredContacts.length > 0) {
|
||||
// Check if a specific uid is requested
|
||||
if($routeParams.uid && $routeParams.gid) {
|
||||
ctrl.filteredContacts.forEach(function(contact) {
|
||||
if(contact.uid() === $routeParams.uid) {
|
||||
ctrl.setSelectedId($routeParams.uid);
|
||||
ctrl.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// No contact previously loaded, let's load the first of the list if not in mobile mode
|
||||
if(ctrl.loading && $(window).width() > 768) {
|
||||
ctrl.setSelectedId(ctrl.filteredContacts[0].uid());
|
||||
}
|
||||
// Get full data for the first 20 contacts of the list
|
||||
ContactService.getFullContacts(ctrl.filteredContacts.slice(0, 20));
|
||||
ctrl.loading = false;
|
||||
unbindListWatch();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.routeParams.uid', function(newValue, oldValue) {
|
||||
// Used for mobile view to clear the url
|
||||
if(typeof oldValue != 'undefined' && typeof newValue == 'undefined' && $(window).width() <= 768) {
|
||||
// no contact selected
|
||||
ctrl.show = true;
|
||||
return;
|
||||
}
|
||||
if(newValue === undefined) {
|
||||
// we might have to wait until ng-repeat filled the contactList
|
||||
if(ctrl.filteredContacts && ctrl.filteredContacts.length > 0) {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: ctrl.filteredContacts[0].uid()
|
||||
});
|
||||
} else {
|
||||
// watch for next contactList update
|
||||
var unbindWatch = $scope.$watch('ctrl.filteredContacts', function() {
|
||||
if(ctrl.filteredContacts && ctrl.filteredContacts.length > 0) {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: ctrl.filteredContacts[0].uid()
|
||||
});
|
||||
}
|
||||
unbindWatch(); // unbind as we only want one update
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// displaying contact details
|
||||
ctrl.show = false;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.routeParams.gid', function() {
|
||||
// we might have to wait until ng-repeat filled the contactList
|
||||
ctrl.filteredContacts = [];
|
||||
ctrl.resetLimitTo();
|
||||
// not in mobile mode
|
||||
if($(window).width() > 768) {
|
||||
// watch for next contactList update
|
||||
var unbindWatch = $scope.$watch('ctrl.filteredContacts', function() {
|
||||
if(ctrl.filteredContacts && ctrl.filteredContacts.length > 0) {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: $routeParams.uid || ctrl.filteredContacts[0].uid()
|
||||
});
|
||||
}
|
||||
unbindWatch(); // unbind as we only want one update
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch if we have an invalid contact
|
||||
$scope.$watch('ctrl.filteredContacts[0].displayName()', function(displayName) {
|
||||
ctrl.invalid = (displayName === '');
|
||||
});
|
||||
|
||||
ctrl.hasContacts = function () {
|
||||
if (!ctrl.contactList) {
|
||||
return false;
|
||||
}
|
||||
return ctrl.contactList.length > 0;
|
||||
};
|
||||
|
||||
ctrl.setSelectedId = function (contactId) {
|
||||
$route.updateParams({
|
||||
uid: contactId
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.getSelectedId = function() {
|
||||
return $routeParams.uid;
|
||||
};
|
||||
|
||||
ctrl.selectNearestContact = function(contactId) {
|
||||
if (ctrl.filteredContacts.length === 1) {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: undefined
|
||||
});
|
||||
} else {
|
||||
for (var i = 0, length = ctrl.filteredContacts.length; i < length; i++) {
|
||||
// Get nearest contact
|
||||
if (ctrl.filteredContacts[i].uid() === contactId) {
|
||||
$route.updateParams({
|
||||
gid: $routeParams.gid,
|
||||
uid: (ctrl.filteredContacts[i+1]) ? ctrl.filteredContacts[i+1].uid() : ctrl.filteredContacts[i-1].uid()
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('contactlist', function() {
|
||||
return {
|
||||
priority: 1,
|
||||
scope: {},
|
||||
controller: 'contactlistCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
addressbook: '=adrbook'
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/contactList.html')
|
||||
};
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('datepicker', function($timeout) {
|
||||
var loadDatepicker = function (scope, element, attrs, ngModelCtrl) {
|
||||
$timeout(function() {
|
||||
element.datepicker({
|
||||
dateFormat:'yy-mm-dd',
|
||||
minDate: null,
|
||||
maxDate: null,
|
||||
constrainInput: false,
|
||||
onSelect:function (date, dp) {
|
||||
if (dp.selectedYear < 1000) {
|
||||
date = '0' + date;
|
||||
}
|
||||
if (dp.selectedYear < 100) {
|
||||
date = '0' + date;
|
||||
}
|
||||
if (dp.selectedYear < 10) {
|
||||
date = '0' + date;
|
||||
}
|
||||
ngModelCtrl.$setViewValue(date);
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
restrict: 'A',
|
||||
require : 'ngModel',
|
||||
transclude: true,
|
||||
link : loadDatepicker
|
||||
};
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('detailsItemCtrl', function($templateRequest, $filter, vCardPropertiesService, ContactService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.meta = vCardPropertiesService.getMeta(ctrl.name);
|
||||
ctrl.type = undefined;
|
||||
ctrl.isPreferred = false;
|
||||
ctrl.t = {
|
||||
poBox : t('contacts', 'Post office box'),
|
||||
postalCode : t('contacts', 'Postal code'),
|
||||
city : t('contacts', 'City'),
|
||||
state : t('contacts', 'State or province'),
|
||||
country : t('contacts', 'Country'),
|
||||
address: t('contacts', 'Address'),
|
||||
newGroup: t('contacts', '(new group)'),
|
||||
familyName: t('contacts', 'Last name'),
|
||||
firstName: t('contacts', 'First name'),
|
||||
additionalNames: t('contacts', 'Additional names'),
|
||||
honorificPrefix: t('contacts', 'Prefix'),
|
||||
honorificSuffix: t('contacts', 'Suffix'),
|
||||
delete: t('contacts', 'Delete')
|
||||
};
|
||||
|
||||
ctrl.availableOptions = ctrl.meta.options || [];
|
||||
if (!_.isUndefined(ctrl.data) && !_.isUndefined(ctrl.data.meta) && !_.isUndefined(ctrl.data.meta.type)) {
|
||||
// parse type of the property
|
||||
var array = ctrl.data.meta.type[0].split(',');
|
||||
array = array.map(function (elem) {
|
||||
return elem.trim().replace(/\/+$/, '').replace(/\\+$/, '').trim().toUpperCase();
|
||||
});
|
||||
// the pref value is handled on its own so that we can add some favorite icon to the ui if we want
|
||||
if (array.indexOf('PREF') >= 0) {
|
||||
ctrl.isPreferred = true;
|
||||
array.splice(array.indexOf('PREF'), 1);
|
||||
}
|
||||
// simply join the upper cased types together as key
|
||||
ctrl.type = array.join(',');
|
||||
var displayName = array.map(function (element) {
|
||||
return element.charAt(0).toUpperCase() + element.slice(1).toLowerCase();
|
||||
}).join(' ');
|
||||
// in case the type is not yet in the default list of available options we add it
|
||||
if (!ctrl.availableOptions.some(function(e) { return e.id === ctrl.type; } )) {
|
||||
ctrl.availableOptions = ctrl.availableOptions.concat([{id: ctrl.type, name: displayName}]);
|
||||
}
|
||||
|
||||
// Remove duplicate entry
|
||||
ctrl.availableOptions = _.uniq(ctrl.availableOptions, function(option) { return option.name; });
|
||||
if (ctrl.availableOptions.filter(function(option) { return option.id === ctrl.type; }).length === 0) {
|
||||
// Our default value has been thrown out by the uniq function, let's find a replacement
|
||||
var optionName = ctrl.meta.options.filter(function(option) { return option.id === ctrl.type; })[0].name;
|
||||
ctrl.type = ctrl.availableOptions.filter(function(option) { return option.name === optionName; })[0].id;
|
||||
// We don't want to override the default keys. Compatibility > standardization
|
||||
// ctrl.data.meta.type[0] = ctrl.type;
|
||||
// ctrl.model.updateContact();
|
||||
}
|
||||
}
|
||||
if (!_.isUndefined(ctrl.data) && !_.isUndefined(ctrl.data.namespace)) {
|
||||
if (!_.isUndefined(ctrl.contact.props['X-ABLABEL'])) {
|
||||
var val = _.find(this.contact.props['X-ABLABEL'], function(x) { return x.namespace === ctrl.data.namespace; });
|
||||
ctrl.type = val.value.toUpperCase();
|
||||
if (!_.isUndefined(val)) {
|
||||
// in case the type is not yet in the default list of available options we add it
|
||||
if (!ctrl.availableOptions.some(function(e) { return e.id === val.value; } )) {
|
||||
ctrl.availableOptions = ctrl.availableOptions.concat([{id: val.value.toUpperCase(), name: val.value.toUpperCase()}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.availableGroups = [];
|
||||
|
||||
ContactService.getGroups().then(function(groups) {
|
||||
ctrl.availableGroups = _.unique(groups);
|
||||
});
|
||||
|
||||
ctrl.changeType = function (val) {
|
||||
if (ctrl.isPreferred) {
|
||||
val += ',PREF';
|
||||
}
|
||||
ctrl.data.meta = ctrl.data.meta || {};
|
||||
ctrl.data.meta.type = ctrl.data.meta.type || [];
|
||||
ctrl.data.meta.type[0] = val;
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.dateInputChanged = function () {
|
||||
ctrl.data.meta = ctrl.data.meta || {};
|
||||
|
||||
var match = ctrl.data.value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (match) {
|
||||
ctrl.data.meta.value = [];
|
||||
} else {
|
||||
ctrl.data.meta.value = ctrl.data.meta.value || [];
|
||||
ctrl.data.meta.value[0] = 'text';
|
||||
}
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.updateDetailedName = function () {
|
||||
var fn = '';
|
||||
if (ctrl.data.value[3]) {
|
||||
fn += ctrl.data.value[3] + ' ';
|
||||
}
|
||||
if (ctrl.data.value[1]) {
|
||||
fn += ctrl.data.value[1] + ' ';
|
||||
}
|
||||
if (ctrl.data.value[2]) {
|
||||
fn += ctrl.data.value[2] + ' ';
|
||||
}
|
||||
if (ctrl.data.value[0]) {
|
||||
fn += ctrl.data.value[0] + ' ';
|
||||
}
|
||||
if (ctrl.data.value[4]) {
|
||||
fn += ctrl.data.value[4];
|
||||
}
|
||||
|
||||
ctrl.contact.fullName(fn);
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.updateContact = function() {
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
|
||||
ctrl.getTemplate = function() {
|
||||
var templateUrl = OC.linkTo('contacts', 'templates/detailItems/' + ctrl.meta.template + '.html');
|
||||
return $templateRequest(templateUrl);
|
||||
};
|
||||
|
||||
ctrl.deleteField = function () {
|
||||
ctrl.contact.removeProperty(ctrl.name, ctrl.data);
|
||||
ContactService.queueUpdate(ctrl.contact);
|
||||
};
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('detailsitem', ['$compile', function($compile) {
|
||||
return {
|
||||
scope: {},
|
||||
controller: 'detailsItemCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
name: '=',
|
||||
data: '=',
|
||||
contact: '=model',
|
||||
index: '='
|
||||
},
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
ctrl.getTemplate().then(function(html) {
|
||||
var template = angular.element(html);
|
||||
element.append(template);
|
||||
$compile(template)(scope);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
|
@ -1,23 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('focusExpression', function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: {
|
||||
post: function postLink(scope, element, attrs) {
|
||||
scope.$watch(attrs.focusExpression, function () {
|
||||
if (attrs.focusExpression) {
|
||||
if (scope.$eval(attrs.focusExpression)) {
|
||||
$timeout(function () {
|
||||
if (element.is('input')) {
|
||||
element.focus();
|
||||
} else {
|
||||
element.find('input').focus();
|
||||
}
|
||||
}, 100); //need some delay to work with ng-disabled
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('groupCtrl', function() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var ctrl = this;
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('group', function() {
|
||||
return {
|
||||
restrict: 'A', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'groupCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
group: '=group'
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/group.html')
|
||||
};
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('grouplistCtrl', function($scope, $timeout, ContactService, SearchService, $routeParams) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.groups = [];
|
||||
ctrl.contactFilters = [];
|
||||
|
||||
ContactService.getGroupList().then(function(groups) {
|
||||
ctrl.groups = groups;
|
||||
});
|
||||
|
||||
ContactService.getContactFilters().then(function(contactFilters) {
|
||||
ctrl.contactFilters = contactFilters;
|
||||
});
|
||||
|
||||
ctrl.getSelected = function() {
|
||||
return $routeParams.gid;
|
||||
};
|
||||
|
||||
// Update groupList on contact add/delete/update/groupsUpdate
|
||||
ContactService.registerObserverCallback(function(ev) {
|
||||
if (ev.event !== 'getFullContacts') {
|
||||
$timeout(function () {
|
||||
$scope.$apply(function() {
|
||||
ContactService.getGroupList().then(function(groups) {
|
||||
ctrl.groups = groups;
|
||||
});
|
||||
ContactService.getContactFilters().then(function(contactFilters) {
|
||||
ctrl.contactFilters = contactFilters;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.setSelected = function (selectedGroup) {
|
||||
SearchService.cleanSearch();
|
||||
$routeParams.gid = selectedGroup;
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('grouplist', function() {
|
||||
return {
|
||||
restrict: 'EA', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'grouplistCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/groupList.html')
|
||||
};
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('importscreenCtrl', function($scope, ImportService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.t = {
|
||||
importingTo : t('contacts', 'Importing into'),
|
||||
selectAddressbook : t('contacts', 'Select your addressbook')
|
||||
};
|
||||
|
||||
// Broadcast update
|
||||
$scope.$on('importing', function () {
|
||||
ctrl.selectedAddressBook = ImportService.selectedAddressBook;
|
||||
ctrl.importedUser = ImportService.importedUser;
|
||||
ctrl.importing = ImportService.importing;
|
||||
ctrl.importPercent = ImportService.importPercent;
|
||||
});
|
||||
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('importscreen', function() {
|
||||
return {
|
||||
restrict: 'EA', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'importscreenCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/importScreen.html')
|
||||
};
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('inputresize', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link : function (scope, element) {
|
||||
var elInput = element.val();
|
||||
element.bind('keydown keyup load focus', function() {
|
||||
elInput = element.val();
|
||||
// If set to 0, the min-width css data is ignored
|
||||
var length = elInput.length > 1 ? elInput.length : 1;
|
||||
element.attr('size', length);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('newContactButtonCtrl', function($scope, ContactService, $routeParams, vCardPropertiesService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.t = {
|
||||
addContact : t('contacts', 'New contact')
|
||||
};
|
||||
|
||||
ctrl.createContact = function() {
|
||||
ContactService.create().then(function(contact) {
|
||||
['tel', 'adr', 'email'].forEach(function(field) {
|
||||
var defaultValue = vCardPropertiesService.getMeta(field).defaultValue || {value: ''};
|
||||
contact.addProperty(field, defaultValue);
|
||||
} );
|
||||
if ([t('contacts', 'All contacts'), t('contacts', 'Not grouped')].indexOf($routeParams.gid) === -1) {
|
||||
contact.categories([ $routeParams.gid ]);
|
||||
} else {
|
||||
contact.categories([]);
|
||||
}
|
||||
$('#details-fullName').focus();
|
||||
});
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('newcontactbutton', function() {
|
||||
return {
|
||||
restrict: 'EA', // has to be an attribute to work with core css
|
||||
scope: {},
|
||||
controller: 'newContactButtonCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/newContactButton.html')
|
||||
};
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('telModel', function() {
|
||||
return{
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attr, ngModel) {
|
||||
ngModel.$formatters.push(function(value) {
|
||||
return value;
|
||||
});
|
||||
ngModel.$parsers.push(function(value) {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('propertyGroupCtrl', function(vCardPropertiesService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.meta = vCardPropertiesService.getMeta(ctrl.name);
|
||||
|
||||
this.isHidden = function() {
|
||||
return ctrl.meta.hasOwnProperty('hidden') && ctrl.meta.hidden === true;
|
||||
};
|
||||
|
||||
this.getIconClass = function() {
|
||||
return ctrl.meta.icon || 'icon-contacts-dark';
|
||||
};
|
||||
|
||||
this.getInfoClass = function() {
|
||||
if (ctrl.meta.hasOwnProperty('info')) {
|
||||
return 'icon-info';
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
this.getInfoText = function() {
|
||||
return ctrl.meta.info;
|
||||
};
|
||||
|
||||
this.getReadableName = function() {
|
||||
return ctrl.meta.readableName;
|
||||
};
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('propertygroup', function() {
|
||||
return {
|
||||
scope: {},
|
||||
controller: 'propertyGroupCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {
|
||||
properties: '=data',
|
||||
name: '=',
|
||||
contact: '=model'
|
||||
},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/propertyGroup.html'),
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
if(ctrl.isHidden()) {
|
||||
// TODO replace with class
|
||||
element.css('display', 'none');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('selectExpression', function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: {
|
||||
post: function postLink(scope, element, attrs) {
|
||||
scope.$watch(attrs.selectExpression, function () {
|
||||
if (attrs.selectExpression) {
|
||||
if (scope.$eval(attrs.selectExpression)) {
|
||||
$timeout(function () {
|
||||
if (element.is('input')) {
|
||||
element.select();
|
||||
} else {
|
||||
element.find('input').select();
|
||||
}
|
||||
}, 100); //need some delay to work with ng-disabled
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.controller('sortbyCtrl', function(SortByService) {
|
||||
var ctrl = this;
|
||||
|
||||
var sortText = t('contacts', 'Sort by');
|
||||
ctrl.sortText = sortText;
|
||||
|
||||
var sortList = SortByService.getSortByList();
|
||||
ctrl.sortList = sortList;
|
||||
|
||||
ctrl.defaultOrder = SortByService.getSortByKey();
|
||||
|
||||
ctrl.updateSortBy = function() {
|
||||
SortByService.setSortBy(ctrl.defaultOrder);
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
angular.module('contactsApp')
|
||||
.directive('sortby', function() {
|
||||
return {
|
||||
priority: 1,
|
||||
scope: {},
|
||||
controller: 'sortbyCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: {},
|
||||
templateUrl: OC.linkTo('contacts', 'templates/sortBy.html')
|
||||
};
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "dav",
|
||||
"homepage": "https://github.com/irgendwie/dav",
|
||||
"_release": "d33ecad1e6",
|
||||
"_resolution": {
|
||||
"type": "commit",
|
||||
"commit": "d33ecad1e64202ec5b35b6911708d1253064b5a7"
|
||||
},
|
||||
"_source": "git@github.com:irgendwie/dav.git",
|
||||
"_target": "d33ecad1e64202ec5b35b6911708d1253064b5a7",
|
||||
"_originalSource": "git@github.com:irgendwie/dav.git"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
SabreDAV/
|
||||
|
||||
/*.zip
|
||||
/build/
|
||||
/coverage/
|
||||
/dav.js.map
|
||||
/dav.min.js
|
||||
/node_modules/
|
|
@ -1,11 +0,0 @@
|
|||
SabreDAV/
|
||||
|
||||
/*.zip
|
||||
/coverage/
|
||||
/Makefile
|
||||
/.git
|
||||
/.gitignore
|
||||
/.jshintrc
|
||||
/.travis.yml
|
||||
/node_modules
|
||||
/test
|
|
@ -1,12 +0,0 @@
|
|||
# This is a bit weird since this is really a js project,
|
||||
# but since we're using sabredav for integration testing
|
||||
# and since travis doesn't support multiple languages
|
||||
# and since travis additionally always installs a base node.js,
|
||||
# the easiest thing to do is pretend to be php.
|
||||
language: php
|
||||
php:
|
||||
- "5.4"
|
||||
before_install:
|
||||
- "nvm install iojs-v1.8.1"
|
||||
install: npm install
|
||||
script: npm test
|
|
@ -1,57 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
|
||||
|
||||
- [Under the hood](#under-the-hood)
|
||||
- [Running the tests](#running-the-tests)
|
||||
- [Publishing a release](#publishing-a-release)
|
||||
- [Related Material](#related-material)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Under the hood
|
||||
|
||||
dav uses npm to manage external dependencies. External npm modules get bundled into the browser js binary with the (excellent) [browserify](http://browserify.org/) utility. dav uses the `DOMParser` and `XMLHttpRequest` web apis (to parse xml and send http requests). All of the async library operations use es6 [Promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise).
|
||||
|
||||
### Running the tests
|
||||
|
||||
```
|
||||
///////////////////////////////////////
|
||||
/ suite / command /
|
||||
///////////////////////////////////////
|
||||
/ integration / make test-integration /
|
||||
///////////////////////////////////////
|
||||
/ lint / make lint /
|
||||
///////////////////////////////////////
|
||||
/ unit / make test-unit /
|
||||
///////////////////////////////////////
|
||||
```
|
||||
|
||||
Things to note:
|
||||
|
||||
+ As of 1.1.1, all of the tests run dav via nodejs. There are no browser tests (yet).
|
||||
+ You can add helpful debug logs to test output with the `DEBUG` environment variable.
|
||||
+ Filter logs by setting `DEBUG=dav:*`, `DEBUG=dav:request:*`, etc.
|
||||
+ Integration tests run against [sabredav](http://sabre.io/)
|
||||
+ The server code lives [here](https://github.com/gaye/dav/blob/master/test/integration/server/calendarserver.php)
|
||||
+ There is a make task which downloads a sabredav release from GitHub that `make test-integration` depends on
|
||||
+ The sabredav instance uses sqlite to store dav collections and objects among other things.
|
||||
+ The code that seeds the database lives [here](https://github.com/gaye/dav/blob/master/test/integration/server/bootstrap.js)
|
||||
|
||||
### Publishing a release
|
||||
|
||||
1. Update `package.json` to reflect the new version. Use [semver](http://semver.org/) to help decide what new version number is best.
|
||||
2. If there are changes to the public api, document them in the README. Then regenerate the `README.md` table of contents with `make toc`.
|
||||
3. Add a new entry to `HISTORY.md` with the new version number and a description of the changeset. Regenerate the `HISTORY.md` table of contents with `make toc`.
|
||||
4. Commit the changes to `package.json`, `HISTORY.md`, and (perhaps) `README.md`. Push to GitHub.
|
||||
5. Run `make && npm publish`.
|
||||
6. Create a new GitHub release named `v.{MAJOR}.{MINOR}.{PATCH}` with a description of the changeset. Upload the freshly generated zipball `dav.zip`.
|
||||
|
||||
### Related Material
|
||||
|
||||
+ [Amazing webdav docs](http://sabre.io/dav/)
|
||||
+ [RFC 4791](http://tools.ietf.org/html/rfc4791)
|
||||
+ [RFC 5545](http://tools.ietf.org/html/rfc5545)
|
||||
+ [RFC 6352](http://tools.ietf.org/html/rfc6352)
|
|
@ -1,271 +0,0 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
|
||||
|
||||
- [1.7.8](#178)
|
||||
- [1.7.7](#177)
|
||||
- [1.7.6](#176)
|
||||
- [1.7.5](#175)
|
||||
- [1.7.4](#174)
|
||||
- [1.7.3](#173)
|
||||
- [1.7.2](#172)
|
||||
- [1.7.0](#170)
|
||||
- [1.6.5](#165)
|
||||
- [1.6.4](#164)
|
||||
- [1.6.3](#163)
|
||||
- [1.6.2](#162)
|
||||
- [1.6.1](#161)
|
||||
- [1.6.0](#160)
|
||||
- [1.5.5](#155)
|
||||
- [1.5.4](#154)
|
||||
- [1.5.3](#153)
|
||||
- [1.5.2](#152)
|
||||
- [1.5.1](#151)
|
||||
- [1.5.0](#150)
|
||||
- [1.4.1](#141)
|
||||
- [1.4.0](#140)
|
||||
- [1.3.0](#130)
|
||||
- [1.2.0](#120)
|
||||
- [1.1.2](#112)
|
||||
- [1.1.1](#111)
|
||||
- [1.1.0](#110)
|
||||
- [1.0.4](#104)
|
||||
- [1.0.3](#103)
|
||||
- [1.0.2](#102)
|
||||
- [1.0.1](#101)
|
||||
- [1.0.0](#100)
|
||||
- [0.11.0](#0110)
|
||||
- [0.10.1](#0101)
|
||||
- [0.10.0](#0100)
|
||||
- [0.9.3](#093)
|
||||
- [0.9.2](#092)
|
||||
- [0.9.1](#091)
|
||||
- [0.9.0](#090)
|
||||
- [0.8.0](#080)
|
||||
- [0.7.1](#071)
|
||||
- [0.7.0](#070)
|
||||
- [0.6.0](#060)
|
||||
- [0.5.0](#050)
|
||||
- [0.4.0](#040)
|
||||
- [0.3.1](#031)
|
||||
- [0.3.0](#030)
|
||||
- [0.2.0](#020)
|
||||
- [0.1.0](#010)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### 1.7.8
|
||||
|
||||
+ `listCalendars` passes through all calendars that contain iCalendar
|
||||
objects and not just `VEVENT`s.
|
||||
|
||||
### 1.7.7
|
||||
|
||||
+ Parser processes href and currentUserPrincipal elements
|
||||
|
||||
### 1.7.6
|
||||
|
||||
+ Bundle `regeneratorRuntime` and polyfills for `Array.prototype.find`
|
||||
and `Object.assign` so that consumers don't need babel polyfill.
|
||||
|
||||
### 1.7.5
|
||||
|
||||
+ Trimmed the binary size to 128k minified
|
||||
+ Remove babel polyfill
|
||||
+ Remove handlebars
|
||||
|
||||
### 1.7.4
|
||||
|
||||
+ Precompile handlebars templates
|
||||
+ Use custom debug module
|
||||
|
||||
### 1.7.3
|
||||
|
||||
+ Google CardDAV wasn't working due to a few issues like our url
|
||||
comparison, Google requiring at least one filter for an addressbook
|
||||
query, and Google sometimes returning propstats with statuses but no
|
||||
props.
|
||||
|
||||
### 1.7.2
|
||||
|
||||
+ Fix outstanding nodejs compatibility issues
|
||||
+ Expose dav.version
|
||||
|
||||
### 1.7.0
|
||||
|
||||
+ Remove lodash, json-stringify-safe, don't expose jsonify
|
||||
|
||||
### 1.6.5
|
||||
|
||||
+ Add brfs as a dependency so that downstream consumers can browserify us
|
||||
|
||||
### 1.6.4
|
||||
|
||||
+ Workaround https://github.com/substack/brfs/issues/39
|
||||
|
||||
### 1.6.3
|
||||
|
||||
+ Convert to use new es6/7 features and transpile with babel
|
||||
|
||||
### 1.6.2
|
||||
|
||||
+ Export debug library under dav ns
|
||||
|
||||
### 1.6.1
|
||||
|
||||
+ Don't bundle xmlhttprequest polyfill in browser binary... again
|
||||
|
||||
### 1.6.0
|
||||
|
||||
+ Add #syncCaldavAccount and #syncCarddavAccount to the public api
|
||||
+ Expose dav.jsonify and dav.ns
|
||||
+ Small correctness fix to error case in basic calendar sync
|
||||
|
||||
### 1.5.5
|
||||
|
||||
+ Bundle XMLHttpRequest polyfill for environments where it's not available
|
||||
|
||||
### 1.5.4
|
||||
|
||||
+ Fix browser globals
|
||||
|
||||
### 1.5.3
|
||||
|
||||
+ Don't use window in web workers
|
||||
|
||||
### 1.5.2
|
||||
|
||||
+ Use xmldom in the browser since it's missing from web workers
|
||||
|
||||
### 1.5.1
|
||||
|
||||
+ Expose dav browserify configuration to npm consumers
|
||||
|
||||
### 1.5.0
|
||||
|
||||
+ Decouple requests from the urls they get sent to
|
||||
|
||||
### 1.4.1
|
||||
|
||||
+ Add missing use strict statement to lib/index.js
|
||||
|
||||
### 1.4.0
|
||||
|
||||
+ New sandbox interface
|
||||
|
||||
### 1.3.0
|
||||
|
||||
+ Expose dav.Model, dav.Request, dav.Transport
|
||||
|
||||
### 1.2.0
|
||||
|
||||
+ Implement client#send
|
||||
|
||||
### 1.1.2
|
||||
|
||||
+ Trick browserify into not bundling node shims for web apis
|
||||
|
||||
### 1.1.1
|
||||
|
||||
+ %s/toString/jsonify/ for models
|
||||
|
||||
### 1.1.0
|
||||
|
||||
+ Support for rfc 6352 carddav
|
||||
|
||||
### 1.0.4
|
||||
|
||||
+ Implement #toString on models
|
||||
|
||||
### 1.0.3
|
||||
|
||||
+ Internal DELETE, PUT request refactor
|
||||
|
||||
### 1.0.2
|
||||
|
||||
+ davinci -> dav
|
||||
|
||||
### 1.0.1
|
||||
|
||||
+ Fix issues with browserify build
|
||||
|
||||
### 1.0.0
|
||||
|
||||
+ Update interfaces for pluggable transports, expose transport layer
|
||||
+ Support for oauth2 authentication
|
||||
+ Clean up internal multistatus parser
|
||||
|
||||
### 0.11.0
|
||||
|
||||
+ Support for rfc 6578 webdav sync
|
||||
|
||||
### 0.10.1
|
||||
|
||||
+ Set request depth to 0 in the "getctag" propfind issued during sync
|
||||
|
||||
### 0.10.0
|
||||
|
||||
+ Implement time-range filters for calendar queries
|
||||
|
||||
### 0.9.3
|
||||
|
||||
+ Remove dependencies on ical.js and underscore
|
||||
|
||||
### 0.9.2
|
||||
|
||||
+ Fix npm package
|
||||
+ Change npm name to davincijs
|
||||
|
||||
### 0.9.1
|
||||
|
||||
+ remove nodejs polyfills for DOMParser and XMLHttpRequest from build output
|
||||
+ generate minified binaries
|
||||
|
||||
### 0.9.0
|
||||
|
||||
+ Implement davinci.Client interface
|
||||
+ Add transport layer to decouple request details and sending
|
||||
|
||||
### 0.8.0
|
||||
|
||||
+ Expose low-level request methods through davinci.request
|
||||
+ Add hook to requests to override transformResponse
|
||||
|
||||
### 0.7.1
|
||||
|
||||
+ Expose the underlying, xml parsed dav responses on davinci.Calendar and davinci.CalendarObject models.
|
||||
|
||||
### 0.7.0
|
||||
|
||||
+ Support providing timezone option to #createAccount and #syncCalendar
|
||||
|
||||
### 0.6.0
|
||||
|
||||
+ #syncCalendar added to public api
|
||||
+ The promise returned from #createAccount now resolves with a davinci.Account object instead of an array of davinci.Calendar objects.
|
||||
|
||||
### 0.5.0
|
||||
|
||||
+ #deleteCalendarObject added to public api
|
||||
|
||||
### 0.4.0
|
||||
|
||||
+ #updateCalendarObject added to public api
|
||||
+ Internal api refactoring to expose Request objects
|
||||
|
||||
### 0.3.1
|
||||
|
||||
+ Patch bug in build due to bug in brfs.
|
||||
|
||||
### 0.3.0
|
||||
|
||||
+ #createCalendarObject modified to support sandboxing.
|
||||
|
||||
### 0.2.0
|
||||
|
||||
+ #createCalendarObject added to public api
|
||||
|
||||
### 0.1.0
|
||||
|
||||
+ #createAccount added to public api
|
||||
+ #createSandbox added to public api
|
363
js/dav/LICENSE
363
js/dav/LICENSE
|
@ -1,363 +0,0 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
HBS := $(shell find lib/template/ -name "*.hbs")
|
||||
JS := $(shell find lib/ -name "*.js")
|
||||
|
||||
SABRE_DAV_VERSION=2.0.1
|
||||
SABRE_DAV_RELEASE=sabredav-$(SABRE_DAV_VERSION)
|
||||
SABRE_DAV_ZIPBALL=$(SABRE_DAV_RELEASE).zip
|
||||
|
||||
dav.zip: dav.js dav.min.js dav.js.map
|
||||
zip dav dav.js dav.js.map dav.min.js
|
||||
|
||||
dav.min.js dav.js.map: dav.js node_modules
|
||||
./node_modules/.bin/uglifyjs dav.js \
|
||||
--lint \
|
||||
--screw-ie8 \
|
||||
--output ./dav.min.js \
|
||||
--source-map ./dav.js.map
|
||||
|
||||
dav.js: build node_modules
|
||||
rm -rf dav.js /tmp/dav.js
|
||||
./node_modules/.bin/browserify --standalone dav ./build/index.js > /tmp/dav.js
|
||||
cat lib/polyfill/*.js /tmp/dav.js > dav.js
|
||||
|
||||
build: $(JS) $(HBS) node_modules
|
||||
rm -rf build/
|
||||
./node_modules/.bin/babel lib \
|
||||
--modules common \
|
||||
--out-dir build \
|
||||
--stage 4
|
||||
|
||||
node_modules: package.json
|
||||
npm install
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf *.zip SabreDAV build coverage dav.* node_modules test/integration/server/SabreDAV
|
||||
|
||||
.PHONY: test
|
||||
test: test-unit test-integration
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: node_modules
|
||||
./node_modules/.bin/mocha test/unit
|
||||
|
||||
.PHONY: test-integration
|
||||
test-integration: node_modules test/integration/server/SabreDAV
|
||||
./node_modules/.bin/mocha test/integration
|
||||
|
||||
.PHONY: toc
|
||||
toc: node_modules
|
||||
./node_modules/.bin/doctoc CONTRIBUTING.md
|
||||
./node_modules/.bin/doctoc HISTORY.md
|
||||
./node_modules/.bin/doctoc README.md
|
||||
|
||||
test/integration/server/SabreDAV: SabreDAV
|
||||
cp -r SabreDAV test/integration/server/SabreDAV
|
||||
cd test/integration/server/SabreDAV && cp ../calendarserver.php calendarserver.php
|
||||
|
||||
SabreDAV:
|
||||
wget -O $(SABRE_DAV_ZIPBALL) https://github.com/fruux/sabre-dav/releases/download/$(SABRE_DAV_VERSION)/$(SABRE_DAV_ZIPBALL)
|
||||
unzip -q $(SABRE_DAV_ZIPBALL)
|
467
js/dav/README.md
467
js/dav/README.md
|
@ -1,467 +0,0 @@
|
|||
dav
|
||||
===
|
||||
|
||||
[![Build Status](https://travis-ci.org/gaye/dav.png?branch=master)](https://travis-ci.org/gaye/dav)
|
||||
|
||||
WebDAV, CalDAV, and CardDAV client for nodejs and the browser.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
|
||||
|
||||
- [API](#api)
|
||||
- [accounts](#accounts)
|
||||
- [dav.createAccount(options)](#davcreateaccountoptions)
|
||||
- [calendars](#calendars)
|
||||
- [dav.createCalendarObject(calendar, options)](#davcreatecalendarobjectcalendar-options)
|
||||
- [dav.updateCalendarObject(calendarObject, options)](#davupdatecalendarobjectcalendarobject-options)
|
||||
- [dav.deleteCalendarObject(calendarObject, options)](#davdeletecalendarobjectcalendarobject-options)
|
||||
- [dav.syncCalendar(calendar, options)](#davsynccalendarcalendar-options)
|
||||
- [dav.syncCaldavAccount(account, options)](#davsynccaldavaccountaccount-options)
|
||||
- [contacts](#contacts)
|
||||
- [dav.createCard(addressBook, options)](#davcreatecardaddressbook-options)
|
||||
- [dav.updateCard(card, options)](#davupdatecardcard-options)
|
||||
- [dav.deleteCard(card, options)](#davdeletecardcard-options)
|
||||
- [dav.syncAddressBook(addressBook, options)](#davsyncaddressbookaddressbook-options)
|
||||
- [dav.syncCarddavAccount(account, options)](#davsynccarddavaccountaccount-options)
|
||||
- [sandbox](#sandbox)
|
||||
- [dav.Sandbox()](#davsandbox)
|
||||
- [transport](#transport)
|
||||
- [dav.transport.Basic(credentials)](#davtransportbasiccredentials)
|
||||
- [dav.transport.Basic.send(request, options)](#davtransportbasicsendrequest-options)
|
||||
- [dav.transport.OAuth2(credentials)](#davtransportoauth2credentials)
|
||||
- [dav.transport.OAuth2.send(request, options)](#davtransportoauth2sendrequest-options)
|
||||
- [request](#request)
|
||||
- [dav.request.addressBookQuery(options)](#davrequestaddressbookqueryoptions)
|
||||
- [dav.request.basic(options)](#davrequestbasicoptions)
|
||||
- [dav.request.calendarQuery(options)](#davrequestcalendarqueryoptions)
|
||||
- [dav.request.propfind(options)](#davrequestpropfindoptions)
|
||||
- [dav.request.syncCollection(options)](#davrequestsynccollectionoptions)
|
||||
- [Client](#client)
|
||||
- [dav.Client(xhr, options)](#davclientxhr-options)
|
||||
- [dav.Client.send(req, options)](#davclientsendreq-options)
|
||||
- [etc](#etc)
|
||||
- [dav.ns](#davns)
|
||||
- [Example Usage](#example-usage)
|
||||
- [Using the lower-level webdav request api](#using-the-lower-level-webdav-request-api)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## API
|
||||
|
||||
### accounts
|
||||
|
||||
#### dav.createAccount(options)
|
||||
|
||||
Perform an initial download of a caldav or carddav account's data. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled with a [dav.Account](https://github.com/gaye/dav/blob/master/lib/model/account.js) object.
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) accountType - one of 'caldav' or 'carddav'. Defaults to 'caldav'.
|
||||
(Array.<Object>) filters - list of caldav filters to send with request.
|
||||
(Boolean) loadCollections - whether or not to load dav collections.
|
||||
(Boolean) loadObjects - whether or not to load dav objects.
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(String) server - some url for server (needn't be base url).
|
||||
(String) timezone - VTIMEZONE calendar object.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
### calendars
|
||||
|
||||
#### dav.createCalendarObject(calendar, options)
|
||||
|
||||
Create a calendar object on the parameter calendar. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the calendar has been created.
|
||||
|
||||
```
|
||||
@param {dav.Calendar} calendar the calendar to put the object on.
|
||||
|
||||
Options:
|
||||
|
||||
(String) data - rfc 5545 VCALENDAR object.
|
||||
(String) filename - name for the calendar ics file.
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.updateCalendarObject(calendarObject, options)
|
||||
|
||||
Persist updates to the parameter calendar object to the server. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the calendar has been updated.
|
||||
|
||||
```
|
||||
@param {dav.CalendarObject} calendarObject updated calendar object.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.deleteCalendarObject(calendarObject, options)
|
||||
|
||||
Delete the parameter calendar object on the server. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the calendar has been deleted.
|
||||
|
||||
```
|
||||
@param {dav.CalendarObject} calendarObject target calendar object.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.syncCalendar(calendar, options)
|
||||
|
||||
Fetch changes from the remote server to the parameter calendar. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled with an updated [dav.Calendar](https://github.com/gaye/dav/blob/master/lib/model/calendar.js) object once sync is complete.
|
||||
|
||||
```
|
||||
@param {dav.Calendar} calendar the calendar to fetch changes for.
|
||||
|
||||
Options:
|
||||
|
||||
(Array.<Object>) filters - list of caldav filters to send with request.
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(String) syncMethod - either 'basic' or 'webdav'. If unspecified, will
|
||||
try to do webdav sync and failover to basic sync if rfc 6578 is not
|
||||
supported by the server.
|
||||
(String) timezone - VTIMEZONE calendar object.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.syncCaldavAccount(account, options)
|
||||
|
||||
Fetch changes from the remote server to the account's calendars. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled with an updated [dav.Account](https://github.com/gaye/dav/blob/master/lib/model/account.js) object once sync is complete.
|
||||
|
||||
```
|
||||
@param {dav.Account} account the calendar account to sync.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
### contacts
|
||||
|
||||
#### dav.createCard(addressBook, options)
|
||||
|
||||
Create a vcard object on the parameter address book. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the vcard has been created.
|
||||
|
||||
```
|
||||
@param {dav.AddressBook} addressBook the address book to put the object on.
|
||||
|
||||
Options:
|
||||
|
||||
(String) data - VCARD object.
|
||||
(String) filename - name for the vcard vcf file.
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.updateCard(card, options)
|
||||
|
||||
Persist updates to the parameter vcard object to the server. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the vcard has been updated.
|
||||
|
||||
```
|
||||
@param {dav.VCard} card updated vcard object.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.deleteCard(card, options)
|
||||
|
||||
Delete the parameter vcard object on the server. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled when the vcard has been deleted.
|
||||
|
||||
```
|
||||
@param {dav.VCard} card target vcard object.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
#### dav.syncAddressBook(addressBook, options)
|
||||
|
||||
Fetch changes from the remote server to the parameter address books. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled with an updated [dav.AddressBook](https://github.com/gaye/dav/blob/master/lib/model/address_book.js) object once sync is complete.
|
||||
|
||||
```
|
||||
@param {dav.AddressBook} addressBook the address book to fetch changes for.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(String) syncMethod - either 'basic' or 'webdav'. If unspecified, will
|
||||
try to do webdav sync and failover to basic sync if rfc 6578 is not
|
||||
supported by the server.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
#### dav.syncCarddavAccount(account, options)
|
||||
|
||||
Fetch changes from the remote server to the account's address books. Returns a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) which will be fulfilled with an updated [dav.Account](https://github.com/gaye/dav/blob/master/lib/model/account.js) object once sync is complete.
|
||||
|
||||
```
|
||||
@param {dav.Account} account the address book account to sync.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(dav.Transport) xhr - request sender.
|
||||
```
|
||||
|
||||
### sandbox
|
||||
|
||||
#### dav.Sandbox()
|
||||
|
||||
Create a request sandbox. There is also a deprecated interface
|
||||
`dav.createSandbox()`. Add requests to the sandbox like so:
|
||||
|
||||
```js
|
||||
var sandbox = new dav.Sandbox();
|
||||
// sandbox instanceof Sandbox
|
||||
dav.createAccount({
|
||||
username: 'Yoshi',
|
||||
password: 'babybowsersoscaryomg',
|
||||
server: 'https://caldav.yoshisstory.com',
|
||||
sandbox: sandbox // <- Insert sandbox here!
|
||||
})
|
||||
.then(function(calendars) {
|
||||
// etc, etc.
|
||||
});
|
||||
```
|
||||
And abort sandboxed requests as a group with `sandbox.abort()`.
|
||||
|
||||
### transport
|
||||
|
||||
#### dav.transport.Basic(credentials)
|
||||
|
||||
Create a new `dav.transport.Basic` object. This sends dav requests using http basic authentication.
|
||||
|
||||
```
|
||||
@param {dav.Credentials} credentials user authorization.
|
||||
```
|
||||
|
||||
##### dav.transport.Basic.send(request, options)
|
||||
|
||||
```
|
||||
@param {dav.Request} request object with request info.
|
||||
@return {Promise} a promise that will be resolved with an xhr request after its readyState is 4 or the result of applying an optional request `transformResponse` function to the xhr object after its readyState is 4.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
```
|
||||
|
||||
#### dav.transport.OAuth2(credentials)
|
||||
|
||||
Create a new `dav.transport.OAuth2` object. This sends dav requests authorized via rfc 6749 oauth2.
|
||||
|
||||
```
|
||||
@param {dav.Credentials} credentials user authorization.
|
||||
```
|
||||
|
||||
##### dav.transport.OAuth2.send(request, options)
|
||||
|
||||
```
|
||||
@param {dav.Request} request object with request info.
|
||||
@return {Promise} a promise that will be resolved with an xhr request after its readyState is 4 or the result of applying an optional request `transformResponse` function to the xhr object after its readyState is 4.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
```
|
||||
|
||||
### request
|
||||
|
||||
#### dav.request.addressBookQuery(options)
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) depth - optional value for Depth header.
|
||||
(Array.<Object>) props - list of props to request.
|
||||
```
|
||||
|
||||
#### dav.request.basic(options)
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) data - put request body.
|
||||
(String) method - http method.
|
||||
(String) etag - cached calendar object etag.
|
||||
```
|
||||
|
||||
#### dav.request.calendarQuery(options)
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) depth - optional value for Depth header.
|
||||
(Array.<Object>) filters - list of filters to send with request.
|
||||
(Array.<Object>) props - list of props to request.
|
||||
(String) timezone - VTIMEZONE calendar object.
|
||||
```
|
||||
|
||||
#### dav.request.propfind(options)
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) depth - optional value for Depth header.
|
||||
(Array.<Object>) props - list of props to request.
|
||||
```
|
||||
|
||||
#### dav.request.syncCollection(options)
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
(String) depth - option value for Depth header.
|
||||
(Array.<Object>) props - list of props to request.
|
||||
(Number) syncLevel - indicates scope of the sync report request.
|
||||
(String) syncToken - synchronization token provided by the server.
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
#### dav.Client(xhr, options)
|
||||
|
||||
Create a new `dav.Client` object. The client interface allows consumers to set their credentials and transport once and then make authorized requests without passing them to each request. Each of the other, public API methods should be available on `dav.Client` objects.
|
||||
|
||||
```
|
||||
@param {dav.Transport} xhr - request sender.
|
||||
|
||||
Options:
|
||||
|
||||
(String) baseUrl - root url to resolve relative request urls with.
|
||||
```
|
||||
|
||||
##### dav.Client.send(req, options)
|
||||
|
||||
Send a request using this client's transport (and perhaps baseUrl).
|
||||
|
||||
```
|
||||
@param {dav.request.Request} req - dav request.
|
||||
@return {Promise} a promise that will be resolved with an xhr request after its readyState is 4 or the result of applying an optional request `transformResponse` function to the xhr object after its readyState is 4.
|
||||
|
||||
Options:
|
||||
|
||||
(dav.Sandbox) sandbox - optional request sandbox.
|
||||
(String) url - relative url for request.
|
||||
```
|
||||
|
||||
### etc
|
||||
|
||||
#### dav.ns
|
||||
|
||||
Object that holds various xml namespace constants.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```js
|
||||
var dav = require('dav');
|
||||
|
||||
var xhr = new dav.transport.Basic(
|
||||
new dav.Credentials({
|
||||
username: 'xxx',
|
||||
password: 'xxx'
|
||||
})
|
||||
);
|
||||
|
||||
dav.createAccount({ server: 'http://dav.example.com', xhr: xhr })
|
||||
.then(function(account) {
|
||||
// account instanceof dav.Account
|
||||
account.calendars.forEach(function(calendar) {
|
||||
console.log('Found calendar named ' + calendar.displayName);
|
||||
// etc.
|
||||
});
|
||||
});
|
||||
|
||||
// Or, using the dav.Client interface:
|
||||
|
||||
var client = new dav.Client(xhr);
|
||||
// No transport arg
|
||||
client.createAccount({
|
||||
server: 'http://dav.example.com',
|
||||
accountType: 'carddav'
|
||||
})
|
||||
.then(function(account) {
|
||||
account.addressBooks.forEach(function(addressBook) {
|
||||
console.log('Found address book name ' + addressBook.displayName);
|
||||
// etc.
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Using the lower-level webdav request api
|
||||
|
||||
_Caution_: The lower-level request api is undergoing some _major_ reworking with frequent changes which will break consumers upgrading from earlier versions. If you're looking for a stable api and can live with the higher-level CalDAV and/or CardDAV abstractions, I _strongly_ recommend those since that api is largely stable.
|
||||
|
||||
```
|
||||
var dav = require('dav');
|
||||
|
||||
var client = new dav.Client(
|
||||
new dav.transport.Basic(
|
||||
new dav.Credentials({
|
||||
username: 'xxx',
|
||||
password: 'xxx'
|
||||
})
|
||||
),
|
||||
{
|
||||
baseUrl: 'https://mail.mozilla.com'
|
||||
}
|
||||
);
|
||||
|
||||
var req = dav.request.basic({
|
||||
method: 'PUT',
|
||||
data: 'BEGIN:VCALENDAR\nEND:VCALENDAR',
|
||||
etag: '12345'
|
||||
});
|
||||
|
||||
// req instanceof dav.Request
|
||||
|
||||
client.send(req, '/calendars/123.ics')
|
||||
.then(function(response) {
|
||||
// response instanceof XMLHttpRequest
|
||||
});
|
||||
```
|
||||
|
||||
Or perhaps without the client:
|
||||
|
||||
```
|
||||
var dav = require('dav');
|
||||
|
||||
var xhr = new dav.transport.Basic(
|
||||
new dav.Credentials({
|
||||
username: 'xxx',
|
||||
password: 'xxx'
|
||||
})
|
||||
);
|
||||
|
||||
// xhr instanceof dav.Transport
|
||||
|
||||
var req = dav.request.basic({
|
||||
method: 'PUT',
|
||||
data: 'BEGIN:VCALENDAR\nEND:VCALENDAR',
|
||||
etag: '12345'
|
||||
});
|
||||
|
||||
// req instanceof dav.Request
|
||||
|
||||
xhr.send(req, 'https://mail.mozilla.com/calendars/123.ics')
|
||||
.then(function(response) {
|
||||
// response instanceof XMLHttpRequest
|
||||
});
|
||||
```
|
||||
|
||||
For more example usages, check out the [suite of integration tests](https://github.com/gaye/dav/tree/master/test/integration).
|
||||
|
||||
## Debugging
|
||||
|
||||
dav can tell you a lot of potentially useful things if you set `dav.debug.enabled = true`.
|
7613
js/dav/dav.js
7613
js/dav/dav.js
File diff suppressed because it is too large
Load Diff
|
@ -1,178 +0,0 @@
|
|||
import co from 'co';
|
||||
import url from 'url';
|
||||
|
||||
import { listCalendars, listCalendarObjects } from './calendars';
|
||||
import { listAddressBooks, listVCards, getFullVcards } from './contacts';
|
||||
import fuzzyUrlEquals from './fuzzy_url_equals';
|
||||
import { Account } from './model';
|
||||
import * as ns from './namespace';
|
||||
import * as request from './request';
|
||||
|
||||
let debug = require('./debug')('dav:accounts');
|
||||
|
||||
let defaults = {
|
||||
accountType: 'caldav',
|
||||
loadCollections: true,
|
||||
loadObjects: false
|
||||
};
|
||||
|
||||
/**
|
||||
* rfc 6764.
|
||||
*
|
||||
* @param {dav.Account} account to find root url for.
|
||||
*/
|
||||
let serviceDiscovery = co.wrap(function *(account, options) {
|
||||
debug('Attempt service discovery.');
|
||||
|
||||
let endpoint = url.parse(account.server);
|
||||
endpoint.protocol = endpoint.protocol || 'http'; // TODO(gareth) https?
|
||||
|
||||
let uri = url.format({
|
||||
protocol: endpoint.protocol,
|
||||
host: endpoint.host,
|
||||
pathname: (!options.useProvidedPath ? '/.well-known/' + options.accountType : endpoint.pathname)
|
||||
});
|
||||
|
||||
let req = request.basic({ method: 'GET' });
|
||||
try {
|
||||
let xhr = yield options.xhr.send(req, uri, { sandbox: options.sandbox });
|
||||
if (xhr.status >= 300 && xhr.status < 400) {
|
||||
// http redirect.
|
||||
let location = xhr.getResponseHeader('Location');
|
||||
if (typeof location === 'string' && location.length) {
|
||||
debug(`Discovery redirected to ${location}`);
|
||||
return url.format({
|
||||
protocol: endpoint.protocol,
|
||||
host: endpoint.host,
|
||||
pathname: location
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debug('Discovery failed... failover to the provided url');
|
||||
}
|
||||
|
||||
return endpoint.href;
|
||||
});
|
||||
|
||||
/**
|
||||
* rfc 5397.
|
||||
*
|
||||
* @param {dav.Account} account to get principal url for.
|
||||
*/
|
||||
let principalUrl = co.wrap(function *(account, options) {
|
||||
debug(`Fetch principal url from context path ${account.rootUrl}.`);
|
||||
let req = request.propfind({
|
||||
props: [ { name: 'current-user-principal', namespace: ns.DAV } ],
|
||||
depth: 0,
|
||||
mergeResponses: true
|
||||
});
|
||||
|
||||
let res = yield options.xhr.send(req, account.rootUrl, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
let container = res.props;
|
||||
debug(`Received principal: ${container.currentUserPrincipal}`);
|
||||
return url.resolve(account.rootUrl, container.currentUserPrincipal);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {dav.Account} account to get home url for.
|
||||
*/
|
||||
let homeUrl = co.wrap(function *(account, options) {
|
||||
debug(`Fetch home url from principal url ${account.principalUrl}.`);
|
||||
let prop;
|
||||
if (options.accountType === 'caldav') {
|
||||
prop = { name: 'calendar-home-set', namespace: ns.CALDAV };
|
||||
} else if (options.accountType === 'carddav') {
|
||||
prop = { name: 'addressbook-home-set', namespace: ns.CARDDAV };
|
||||
}
|
||||
|
||||
var req = request.propfind({ props: [ prop ] });
|
||||
|
||||
let responses = yield options.xhr.send(req, account.principalUrl, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
let response = responses.find(response => {
|
||||
return fuzzyUrlEquals(account.principalUrl, response.href);
|
||||
});
|
||||
|
||||
let container = response.props;
|
||||
let href;
|
||||
if (options.accountType === 'caldav') {
|
||||
debug(`Received home: ${container.calendarHomeSet}`);
|
||||
href = container.calendarHomeSet;
|
||||
} else if (options.accountType === 'carddav') {
|
||||
debug(`Received home: ${container.addressbookHomeSet}`);
|
||||
href = container.addressbookHomeSet;
|
||||
}
|
||||
|
||||
return url.resolve(account.rootUrl, href);
|
||||
});
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) accountType - one of 'caldav' or 'carddav'. Defaults to 'caldav'.
|
||||
* (Array.<Object>) filters - list of caldav filters to send with request.
|
||||
* (Boolean) loadCollections - whether or not to load dav collections.
|
||||
* (Boolean) loadObjects - whether or not to load dav objects.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (String) server - some url for server (needn't be base url).
|
||||
* (String) timezone - VTIMEZONE calendar object.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*
|
||||
* @return {Promise} a promise that will resolve with a dav.Account object.
|
||||
*/
|
||||
exports.createAccount = co.wrap(function *(options) {
|
||||
options = Object.assign({}, defaults, options);
|
||||
if (typeof options.loadObjects !== 'boolean') {
|
||||
options.loadObjects = options.loadCollections;
|
||||
}
|
||||
|
||||
let account = new Account({
|
||||
server: options.server,
|
||||
credentials: options.xhr.credentials
|
||||
});
|
||||
|
||||
account.rootUrl = yield serviceDiscovery(account, options);
|
||||
account.principalUrl = yield principalUrl(account, options);
|
||||
account.homeUrl = yield homeUrl(account, options);
|
||||
|
||||
if (!options.loadCollections) {
|
||||
return account;
|
||||
}
|
||||
|
||||
let key, loadCollections, loadObjects;
|
||||
if (options.accountType === 'caldav') {
|
||||
key = 'calendars';
|
||||
loadCollections = listCalendars;
|
||||
loadObjects = listCalendarObjects;
|
||||
} else if (options.accountType === 'carddav') {
|
||||
key = 'addressBooks';
|
||||
loadCollections = listAddressBooks;
|
||||
loadObjects = listVCards;
|
||||
}
|
||||
|
||||
var collections = yield loadCollections(account, options);
|
||||
account[key] = collections;
|
||||
if (!options.loadObjects) {
|
||||
return account;
|
||||
}
|
||||
|
||||
yield collections.map(co.wrap(function *(collection) {
|
||||
try {
|
||||
collection.objects = yield loadObjects(collection, options);
|
||||
} catch (error) {
|
||||
collection.error = error;
|
||||
}
|
||||
}));
|
||||
|
||||
account[key] = account[key].filter(function(collection) {
|
||||
return !collection.error;
|
||||
});
|
||||
|
||||
return account;
|
||||
});
|
|
@ -1,274 +0,0 @@
|
|||
import co from 'co';
|
||||
import url from 'url';
|
||||
|
||||
import fuzzyUrlEquals from './fuzzy_url_equals';
|
||||
import { Calendar, CalendarObject } from './model';
|
||||
import * as ns from './namespace';
|
||||
import * as request from './request';
|
||||
import * as webdav from './webdav';
|
||||
|
||||
let debug = require('./debug')('dav:calendars');
|
||||
|
||||
const ICAL_OBJS = new Set([
|
||||
'VEVENT',
|
||||
'VTODO',
|
||||
'VJOURNAL',
|
||||
'VFREEBUSY',
|
||||
'VTIMEZONE',
|
||||
'VALARM'
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {dav.Account} account to fetch calendars for.
|
||||
*/
|
||||
export let listCalendars = co.wrap(function *(account, options) {
|
||||
debug(`Fetch calendars from home url ${account.homeUrl}`);
|
||||
var req = request.propfind({
|
||||
props: [
|
||||
{ name: 'calendar-description', namespace: ns.CALDAV },
|
||||
{ name: 'calendar-timezone', namespace: ns.CALDAV },
|
||||
{ name: 'displayname', namespace: ns.DAV },
|
||||
{ name: 'getctag', namespace: ns.CALENDAR_SERVER },
|
||||
{ name: 'resourcetype', namespace: ns.DAV },
|
||||
{ name: 'supported-calendar-component-set', namespace: ns.CALDAV },
|
||||
{ name: 'sync-token', namespace: ns.DAV }
|
||||
],
|
||||
depth: 1
|
||||
});
|
||||
|
||||
let responses = yield options.xhr.send(req, account.homeUrl, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
debug(`Found ${responses.length} calendars.`);
|
||||
let cals = responses
|
||||
.filter(res => {
|
||||
// We only want the calendar if it contains iCalendar objects.
|
||||
let components = res.props.supportedCalendarComponentSet || [];
|
||||
return components.reduce((hasObjs, component) => {
|
||||
return hasObjs || ICAL_OBJS.has(component)
|
||||
}, false)
|
||||
})
|
||||
.map(res => {
|
||||
debug(`Found calendar ${res.props.displayname},
|
||||
props: ${JSON.stringify(res.props)}`);
|
||||
return new Calendar({
|
||||
data: res,
|
||||
account: account,
|
||||
description: res.props.calendarDescription,
|
||||
timezone: res.props.calendarTimezone,
|
||||
url: url.resolve(account.rootUrl, res.href),
|
||||
ctag: res.props.getctag,
|
||||
displayName: res.props.displayname,
|
||||
components: res.props.supportedCalendarComponentSet,
|
||||
resourcetype: res.props.resourcetype,
|
||||
syncToken: res.props.syncToken
|
||||
});
|
||||
});
|
||||
|
||||
yield cals.map(co.wrap(function *(cal) {
|
||||
cal.reports = yield webdav.supportedReportSet(cal, options);
|
||||
}));
|
||||
|
||||
return cals;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {dav.Calendar} calendar the calendar to put the object on.
|
||||
* @return {Promise} promise will resolve when the calendar has been created.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (String) data - rfc 5545 VCALENDAR object.
|
||||
* (String) filename - name for the calendar ics file.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function createCalendarObject(calendar, options) {
|
||||
var objectUrl = url.resolve(calendar.url, options.filename);
|
||||
return webdav.createObject(objectUrl, options.data, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {dav.CalendarObject} calendarObject updated calendar object.
|
||||
* @return {Promise} promise will resolve when the calendar has been updated.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function updateCalendarObject(calendarObject, options) {
|
||||
return webdav.updateObject(
|
||||
calendarObject.url,
|
||||
calendarObject.calendarData,
|
||||
calendarObject.etag,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.CalendarObject} calendarObject target calendar object.
|
||||
* @return {Promise} promise will resolve when the calendar has been deleted.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function deleteCalendarObject(calendarObject, options) {
|
||||
return webdav.deleteObject(
|
||||
calendarObject.url,
|
||||
calendarObject.etag,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.Calendar} calendar the calendar to fetch objects for.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (Array.<Object>) filters - optional caldav filters.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export let listCalendarObjects = co.wrap(function *(calendar, options) {
|
||||
debug(`Doing REPORT on calendar ${calendar.url} which belongs to
|
||||
${calendar.account.credentials.username}`);
|
||||
|
||||
let filters = options.filters || [{
|
||||
type: 'comp-filter',
|
||||
attrs: { name: 'VCALENDAR' },
|
||||
children: [{
|
||||
type: 'comp-filter',
|
||||
attrs: { name: 'VEVENT' }
|
||||
}]
|
||||
}];
|
||||
|
||||
let req = request.calendarQuery({
|
||||
depth: 1,
|
||||
props: [
|
||||
{ name: 'getetag', namespace: ns.DAV },
|
||||
{ name: 'calendar-data', namespace: ns.CALDAV }
|
||||
],
|
||||
filters: filters
|
||||
});
|
||||
|
||||
let responses = yield options.xhr.send(req, calendar.url, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
return responses.map(res => {
|
||||
debug(`Found calendar object with url ${res.href}`);
|
||||
return new CalendarObject({
|
||||
data: res,
|
||||
calendar: calendar,
|
||||
url: url.resolve(calendar.account.rootUrl, res.href),
|
||||
etag: res.props.getetag,
|
||||
calendarData: res.props.calendarData
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {dav.Calendar} calendar the calendar to fetch updates to.
|
||||
* @return {Promise} promise will resolve with updated calendar object.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (Array.<Object>) filters - list of caldav filters to send with request.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (String) syncMethod - either 'basic' or 'webdav'. If unspecified, will
|
||||
* try to do webdav sync and failover to basic sync if rfc 6578 is not
|
||||
* supported by the server.
|
||||
* (String) timezone - VTIMEZONE calendar object.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function syncCalendar(calendar, options) {
|
||||
options.basicSync = basicSync;
|
||||
options.webdavSync = webdavSync;
|
||||
return webdav.syncCollection(calendar, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.Account} account the account to fetch updates for.
|
||||
* @return {Promise} promise will resolve with updated account.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export let syncCaldavAccount = co.wrap(function *(account, options={}) {
|
||||
options.loadObjects = false;
|
||||
if (!account.calendars) account.calendars = [];
|
||||
|
||||
let cals = yield listCalendars(account, options);
|
||||
cals
|
||||
.filter(cal => {
|
||||
// Filter the calendars not previously seen.
|
||||
return account.calendars.every(prev => !fuzzyUrlEquals(prev.url, cal.url));
|
||||
})
|
||||
.forEach(cal => {
|
||||
// Add them to the account's calendar list.
|
||||
account.calendars.push(cal);
|
||||
});
|
||||
|
||||
options.loadObjects = true;
|
||||
yield account.calendars.map(co.wrap(function *(cal, index) {
|
||||
try {
|
||||
yield syncCalendar(cal, options);
|
||||
} catch (error) {
|
||||
debug(`Sync calendar ${cal.displayName} failed with ${error}`);
|
||||
account.calendars.splice(index, 1);
|
||||
}
|
||||
}));
|
||||
|
||||
return account;
|
||||
});
|
||||
|
||||
let basicSync = co.wrap(function *(calendar, options) {
|
||||
let sync = yield webdav.isCollectionDirty(calendar, options);
|
||||
if (!sync) {
|
||||
debug('Local ctag matched remote! No need to sync :).');
|
||||
return calendar;
|
||||
}
|
||||
|
||||
debug('ctag changed so we need to fetch stuffs.');
|
||||
calendar.objects = yield listCalendarObjects(calendar, options);
|
||||
return calendar;
|
||||
});
|
||||
|
||||
let webdavSync = co.wrap(function *(calendar, options) {
|
||||
var req = request.syncCollection({
|
||||
props: [
|
||||
{ name: 'getetag', namespace: ns.DAV },
|
||||
{ name: 'calendar-data', namespace: ns.CALDAV }
|
||||
],
|
||||
syncLevel: 1,
|
||||
syncToken: calendar.syncToken
|
||||
});
|
||||
|
||||
let result = yield options.xhr.send(req, calendar.url, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
// TODO(gareth): Handle creations and deletions.
|
||||
result.responses.forEach(function(response) {
|
||||
// Find the calendar object that this response corresponds with.
|
||||
var calendarObject = calendar.objects.filter(function(object) {
|
||||
return fuzzyUrlEquals(object.url, response.href);
|
||||
})[0];
|
||||
|
||||
if (!calendarObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
calendarObject.etag = response.props.getetag;
|
||||
calendarObject.calendarData = response.props.calendarData;
|
||||
});
|
||||
|
||||
calendar.syncToken = result.syncToken;
|
||||
return calendar;
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* @fileoverview Camelcase something.
|
||||
*/
|
||||
export default function camelize(str, delimiter='_') {
|
||||
let words = str.split(delimiter);
|
||||
return [words[0]]
|
||||
.concat(
|
||||
words.slice(1).map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
)
|
||||
.join('');
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
import url from 'url';
|
||||
|
||||
import * as accounts from './accounts';
|
||||
import * as calendars from './calendars';
|
||||
import * as contacts from './contacts';
|
||||
|
||||
/**
|
||||
* @param {dav.Transport} xhr - request sender.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (String) baseUrl - root url to resolve relative request urls with.
|
||||
*/
|
||||
export class Client {
|
||||
constructor(xhr, options={}) {
|
||||
this.xhr = xhr;
|
||||
Object.assign(this, options);
|
||||
|
||||
// Expose internal modules for unit testing
|
||||
this._accounts = accounts;
|
||||
this._calendars = calendars;
|
||||
this._contacts = contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.Request} req - dav request.
|
||||
* @param {String} uri - where to send request.
|
||||
* @return {Promise} a promise that will be resolved with an xhr request
|
||||
* after its readyState is 4 or the result of applying an optional
|
||||
* request `transformResponse` function to the xhr object after its
|
||||
* readyState is 4.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (Object) sandbox - optional request sandbox.
|
||||
*/
|
||||
send(req, uri, options) {
|
||||
if (this.baseUrl) {
|
||||
let urlObj = url.parse(uri);
|
||||
uri = url.resolve(this.baseUrl, urlObj.path);
|
||||
}
|
||||
|
||||
return this.xhr.send(req, uri, options);
|
||||
}
|
||||
|
||||
createAccount(options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return accounts.createAccount(options);
|
||||
}
|
||||
|
||||
createCalendarObject(calendar, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return calendars.createCalendarObject(calendar, options);
|
||||
}
|
||||
|
||||
updateCalendarObject(calendarObject, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return calendars.updateCalendarObject(calendarObject, options);
|
||||
}
|
||||
|
||||
deleteCalendarObject(calendarObject, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return calendars.deleteCalendarObject(calendarObject, options);
|
||||
}
|
||||
|
||||
syncCalendar(calendar, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return calendars.syncCalendar(calendar, options);
|
||||
}
|
||||
|
||||
syncCaldavAccount(account, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return calendars.syncCaldavAccount(account, options);
|
||||
}
|
||||
|
||||
getAddressBook(options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.getAddressBook(options);
|
||||
}
|
||||
|
||||
createAddressBook(options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.createAddressBook(options);
|
||||
}
|
||||
|
||||
deleteAddressBook(addressBook, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.deleteAddressBook(addressBook, options);
|
||||
}
|
||||
|
||||
renameAddressBook(addressBook, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.renameAddressBook(addressBook, options);
|
||||
}
|
||||
|
||||
createCard(addressBook, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.createCard(addressBook, options);
|
||||
}
|
||||
|
||||
updateCard(card, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.updateCard(card, options);
|
||||
}
|
||||
|
||||
deleteCard(card, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.deleteCard(card, options);
|
||||
}
|
||||
|
||||
getContacts(addressBook, options={}, hrefs) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.getContacts(addressBook, options, hrefs);
|
||||
}
|
||||
|
||||
syncAddressBook(addressBook, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.syncAddressBook(addressBook, options);
|
||||
}
|
||||
|
||||
syncCarddavAccount(account, options={}) {
|
||||
options.xhr = options.xhr || this.xhr;
|
||||
return contacts.syncCarddavAccount(account, options);
|
||||
}
|
||||
}
|
|
@ -1,350 +0,0 @@
|
|||
import co from 'co';
|
||||
import url from 'url';
|
||||
|
||||
import fuzzyUrlEquals from './fuzzy_url_equals';
|
||||
import { AddressBook, VCard } from './model';
|
||||
import * as ns from './namespace';
|
||||
import * as request from './request';
|
||||
import * as webdav from './webdav';
|
||||
|
||||
let debug = require('./debug')('dav:contacts');
|
||||
|
||||
/**
|
||||
* @param {dav.Account} account to fetch address books for.
|
||||
*/
|
||||
export let listAddressBooks = co.wrap(function *(account, options) {
|
||||
debug(`Fetch address books from home url ${account.homeUrl}`);
|
||||
var req = request.propfind({
|
||||
props: [
|
||||
{ name: 'displayname', namespace: ns.DAV },
|
||||
{ name: 'owner', namespace: ns.DAV },
|
||||
{ name: 'getctag', namespace: ns.CALENDAR_SERVER },
|
||||
{ name: 'resourcetype', namespace: ns.DAV },
|
||||
{ name: 'sync-token', namespace: ns.DAV },
|
||||
{ name: 'read-only', namespace: ns.OC },
|
||||
//{ name: 'groups', namespace: ns.OC },
|
||||
{ name: 'invite', namespace: ns.OC },
|
||||
{ name: 'enabled', namespace: ns.OC }
|
||||
],
|
||||
depth: 1
|
||||
});
|
||||
|
||||
let responses = yield options.xhr.send(req, account.homeUrl, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
let addressBooks = responses
|
||||
.filter(res => {
|
||||
return typeof res.props.displayname === 'string';
|
||||
})
|
||||
.map(res => {
|
||||
debug(`Found address book named ${res.props.displayname},
|
||||
props: ${JSON.stringify(res.props)}`);
|
||||
return new AddressBook({
|
||||
data: res,
|
||||
account: account,
|
||||
url: url.resolve(account.rootUrl, res.href),
|
||||
ctag: res.props.getctag,
|
||||
displayName: res.props.displayname,
|
||||
resourcetype: res.props.resourcetype,
|
||||
syncToken: res.props.syncToken
|
||||
});
|
||||
});
|
||||
|
||||
yield addressBooks.map(co.wrap(function *(addressBook) {
|
||||
addressBook.reports = yield webdav.supportedReportSet(addressBook, options);
|
||||
}));
|
||||
|
||||
return addressBooks;
|
||||
});
|
||||
|
||||
export function getAddressBook(options) {
|
||||
let addressBookUrl = url.resolve(options.url, options.displayName);
|
||||
var req = request.propfind({
|
||||
props: [
|
||||
{ name: 'displayname', namespace: ns.DAV },
|
||||
{ name: 'owner', namespace: ns.DAV },
|
||||
{ name: 'getctag', namespace: ns.CALENDAR_SERVER },
|
||||
{ name: 'resourcetype', namespace: ns.DAV },
|
||||
{ name: 'sync-token', namespace: ns.DAV },
|
||||
//{ name: 'groups', namespace: ns.OC },
|
||||
{ name: 'invite', namespace: ns.OC }
|
||||
],
|
||||
depth: 1
|
||||
});
|
||||
|
||||
return options.xhr.send(req, addressBookUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise} promise will resolve when the addressBook has been created.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (String) url
|
||||
* (String) displayName - name for the address book.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function createAddressBook(options) {
|
||||
let collectionUrl = url.resolve(options.url, options.displayName);
|
||||
options.props = [
|
||||
{ name: 'resourcetype', namespace: ns.DAV, children: [
|
||||
{ name: 'collection', namespace: ns.DAV },
|
||||
{ name: 'addressbook', namespace: ns.CARDDAV }
|
||||
]
|
||||
},
|
||||
{ name: 'displayname', value: options.displayName, namespace: ns.DAV }
|
||||
]
|
||||
return webdav.createCollection(collectionUrl, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.AddressBook} addressBook the address book to be deleted.
|
||||
* @return {Promise} promise will resolve when the addressBook has been deleted.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function deleteAddressBook(addressBook, options) {
|
||||
return webdav.deleteCollection(addressBook.url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.AddressBook} addressBook the address book to be renamed.
|
||||
* @return {Promise} promise will resolve when the addressBook has been renamed.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (String) displayName - new name for the address book.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function renameAddressBook(addressBook, options) {
|
||||
options.props = [
|
||||
{ name: 'displayname', value: options.displayName, namespace: ns.DAV }
|
||||
]
|
||||
return webdav.updateProperties(addressBook.url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.AddressBook} addressBook the address book to put the object on.
|
||||
* @return {Promise} promise will resolve when the card has been created.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (String) data - vcard object.
|
||||
* (String) filename - name for the address book vcf file.
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function createCard(addressBook, options) {
|
||||
let objectUrl = url.resolve(addressBook.url, options.filename);
|
||||
return webdav.createObject(objectUrl, options.data, options);
|
||||
}
|
||||
|
||||
export let getFullVcards = co.wrap(function *(addressBook, options, hrefs) {
|
||||
var req = request.addressBookMultiget({
|
||||
depth: 1,
|
||||
props: [
|
||||
{ name: 'getetag', namespace: ns.DAV },
|
||||
{ name: 'address-data', namespace: ns.CARDDAV }
|
||||
],
|
||||
hrefs: hrefs
|
||||
});
|
||||
|
||||
let responses = yield options.xhr.send(req, addressBook.url, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
return responses.map(res => {
|
||||
debug(`Found vcard with url ${res.href}`);
|
||||
return new VCard({
|
||||
data: res,
|
||||
addressBook: addressBook,
|
||||
url: url.resolve(addressBook.account.rootUrl, res.href),
|
||||
etag: res.props.getetag,
|
||||
addressData: res.props.addressData
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
*/
|
||||
export let listVCards = co.wrap(function *(addressBook, options) {
|
||||
debug(`Doing REPORT on address book ${addressBook.url} which belongs to
|
||||
${addressBook.account.credentials.username}`);
|
||||
|
||||
var vCardListFields = [ 'EMAIL', 'UID', 'CATEGORIES', 'FN', 'TEL', 'NICKNAME', 'N' ]
|
||||
.map(function (value) {
|
||||
return {
|
||||
name: 'prop',
|
||||
namespace: ns.CARDDAV,
|
||||
attrs: [ { name: 'name', value: value } ]
|
||||
};
|
||||
});
|
||||
var req = request.addressBookQuery({
|
||||
depth: 1,
|
||||
props: [
|
||||
{ name: 'getetag', namespace: ns.DAV },
|
||||
{ name: 'address-data', namespace: ns.CARDDAV, children: vCardListFields }
|
||||
]
|
||||
});
|
||||
|
||||
let responses = yield options.xhr.send(req, addressBook.url, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
return responses.map(res => {
|
||||
debug(`Found vcard with url ${res.href}`);
|
||||
return new VCard({
|
||||
data: res,
|
||||
addressBook: addressBook,
|
||||
url: url.resolve(addressBook.account.rootUrl, res.href),
|
||||
etag: res.props.getetag,
|
||||
addressData: res.props.addressData
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {dav.VCard} card updated vcard object.
|
||||
* @return {Promise} promise will resolve when the card has been updated.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function updateCard(card, options) {
|
||||
return webdav.updateObject(
|
||||
card.url,
|
||||
card.addressData,
|
||||
card.etag,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.VCard} card target vcard object.
|
||||
* @return {Promise} promise will resolve when the calendar has been deleted.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function deleteCard(card, options) {
|
||||
return webdav.deleteObject(
|
||||
card.url,
|
||||
card.etag,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.Calendar} calendar the calendar to fetch updates to.
|
||||
* @return {Promise} promise will resolve with updated calendar object.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (String) syncMethod - either 'basic' or 'webdav'. If unspecified, will
|
||||
* try to do webdav sync and failover to basic sync if rfc 6578 is not
|
||||
* supported by the server.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export function syncAddressBook(addressBook, options) {
|
||||
options.basicSync = basicSync;
|
||||
options.webdavSync = webdavSync;
|
||||
return webdav.syncCollection(addressBook, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dav.Account} account the account to fetch updates for.
|
||||
* @return {Promise} promise will resolve with updated account.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* (dav.Sandbox) sandbox - optional request sandbox.
|
||||
* (dav.Transport) xhr - request sender.
|
||||
*/
|
||||
export let syncCarddavAccount = co.wrap(function *(account, options={}) {
|
||||
options.loadObjects = false;
|
||||
|
||||
if (!account.addressBooks) {
|
||||
account.addressBooks = [];
|
||||
}
|
||||
|
||||
let addressBooks = yield listAddressBooks(account, options);
|
||||
addressBooks
|
||||
.filter(function(addressBook) {
|
||||
// Filter the address books not previously seen.
|
||||
return account.addressBooks.every(
|
||||
prev => !fuzzyUrlEquals(prev.url, addressBook.url)
|
||||
);
|
||||
})
|
||||
.forEach(addressBook => account.addressBooks.push(addressBook));
|
||||
|
||||
options.loadObjects = true;
|
||||
yield account.addressBooks.map(co.wrap(function *(addressBook, index) {
|
||||
try {
|
||||
yield syncAddressBook(addressBook, options);
|
||||
} catch (error) {
|
||||
debug(`Syncing ${addressBook.displayName} failed with ${error}`);
|
||||
account.addressBooks.splice(index, 1);
|
||||
}
|
||||
}));
|
||||
|
||||
return account;
|
||||
});
|
||||
|
||||
export let getContacts = getFullVcards;
|
||||
|
||||
let basicSync = co.wrap(function *(addressBook, options) {
|
||||
let sync = webdav.isCollectionDirty(addressBook, options)
|
||||
if (!sync) {
|
||||
debug('Local ctag matched remote! No need to sync :).');
|
||||
return addressBook;
|
||||
}
|
||||
|
||||
debug('ctag changed so we need to fetch stuffs.');
|
||||
addressBook.objects = yield listVCards(addressBook, options);
|
||||
return addressBook;
|
||||
});
|
||||
|
||||
let webdavSync = co.wrap(function *(addressBook, options) {
|
||||
var req = request.syncCollection({
|
||||
props: [
|
||||
{ name: 'getetag', namespace: ns.DAV },
|
||||
{ name: 'address-data', namespace: ns.CARDDAV }
|
||||
],
|
||||
syncLevel: 1,
|
||||
syncToken: addressBook.syncToken
|
||||
});
|
||||
|
||||
let result = yield options.xhr.send(req, addressBook.url, {
|
||||
sandbox: options.sandbox
|
||||
});
|
||||
|
||||
// TODO(gareth): Handle creations and deletions.
|
||||
result.responses.forEach(response => {
|
||||
// Find the vcard that this response corresponds with.
|
||||
let vcard = addressBook.objects.filter(object => {
|
||||
return fuzzyUrlEquals(object.url, response.href);
|
||||
})[0];
|
||||
|
||||
if (!vcard) return;
|
||||
|
||||
vcard.etag = response.props.getetag;
|
||||
vcard.addressData = response.props.addressData;
|
||||
});
|
||||
|
||||
addressBook.syncToken = result.syncToken;
|
||||
return addressBook;
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
export default function debug(topic) {
|
||||
return function(message) {
|
||||
if (debug.enabled) {
|
||||
console.log(`[${topic}] ${message}`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
'use strict';
|
||||
export default function fuzzyUrlEquals(one, other) {
|
||||
other = encodeURI(other);
|
||||
return fuzzyIncludes(one, other) || fuzzyIncludes(other, one);
|
||||
};
|
||||
|
||||
function fuzzyIncludes(one, other) {
|
||||
return one.indexOf(other) !== -1 ||
|
||||
(other.charAt(other.length -1) === '/' &&
|
||||
one.indexOf(other.slice(0, -1)) !== -1);
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import debug from './debug';
|
||||
import * as ns from './namespace';
|
||||
import * as request from './request';
|
||||
import * as transport from './transport';
|
||||
|
||||
export { version } from '../package';
|
||||
export { createAccount } from './accounts';
|
||||
export * from './calendars';
|
||||
export { Client } from './client';
|
||||
export * from './contacts';
|
||||
export * from './model';
|
||||
export { Request } from './request';
|
||||
export { Sandbox, createSandbox } from './sandbox';
|
||||
export { debug, ns, request, transport }
|
|
@ -1,106 +0,0 @@
|
|||
export class Account {
|
||||
constructor(options) {
|
||||
Object.assign(this, {
|
||||
server: null,
|
||||
credentials: null,
|
||||
rootUrl: null,
|
||||
principalUrl: null,
|
||||
homeUrl: null,
|
||||
calendars: null,
|
||||
addressBooks: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
* (String) username - username (perhaps email) for calendar user.
|
||||
* (String) password - plaintext password for calendar user.
|
||||
* (String) clientId - oauth client id.
|
||||
* (String) clientSecret - oauth client secret.
|
||||
* (String) authorizationCode - oauth code.
|
||||
* (String) redirectUrl - oauth redirect url.
|
||||
* (String) tokenUrl - oauth token url.
|
||||
* (String) accessToken - oauth access token.
|
||||
* (String) refreshToken - oauth refresh token.
|
||||
* (Number) expiration - unix time for access token expiration.
|
||||
*/
|
||||
export class Credentials {
|
||||
constructor(options) {
|
||||
Object.assign(this, {
|
||||
username: null,
|
||||
password: null,
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
authorizationCode: null,
|
||||
redirectUrl: null,
|
||||
tokenUrl: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
expiration: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class DAVCollection {
|
||||
constructor(options) {
|
||||
Object.assign(this, {
|
||||
data: null,
|
||||
objects: null,
|
||||
account: null,
|
||||
ctag: null,
|
||||
description: null,
|
||||
displayName: null,
|
||||
reports: null,
|
||||
resourcetype: null,
|
||||
syncToken: null,
|
||||
url: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressBook extends DAVCollection {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
|
||||
export class Calendar extends DAVCollection {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
Object.assign(this, {
|
||||
components: null,
|
||||
timezone: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class DAVObject {
|
||||
constructor(options) {
|
||||
Object.assign(this, {
|
||||
data: null,
|
||||
etag: null,
|
||||
url: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class CalendarObject extends DAVObject {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
Object.assign(this, {
|
||||
calendar: null,
|
||||
calendarData: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class VCard extends DAVObject {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
Object.assign(this, {
|
||||
addressBook: null,
|
||||
addressData: null
|
||||
}, options);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export const CALENDAR_SERVER = 'http://calendarserver.org/ns/';
|
||||
export const CALDAV = 'urn:ietf:params:xml:ns:caldav';
|
||||
export const CARDDAV = 'urn:ietf:params:xml:ns:carddav';
|
||||
export const DAV = 'DAV:';
|
||||
export const OC = 'http://owncloud.org/ns';
|
|
@ -1,166 +0,0 @@
|
|||
import camelize from './camelize';
|
||||
|
||||
let debug = require('./debug')('dav:parser');
|
||||
|
||||
let DOMParser = require('xmldom').DOMParser;
|
||||
|
||||
export function multistatus(string) {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(string, 'text/xml');
|
||||
let result = traverse.multistatus(child(doc, 'multistatus'));
|
||||
debug(`input:\n${string}\noutput:\n${JSON.stringify(result)}\n`);
|
||||
return result;
|
||||
}
|
||||
|
||||
let traverse = {
|
||||
// { response: [x, y, z] }
|
||||
multistatus: node => complex(node, { response: true }),
|
||||
|
||||
// { propstat: [x, y, z] }
|
||||
response: node => complex(node, { propstat: true, href: false }),
|
||||
|
||||
// { prop: x }
|
||||
propstat: node => complex(node, { prop: false }),
|
||||
|
||||
// {
|
||||
// resourcetype: x
|
||||
// supportedCalendarComponentSet: y,
|
||||
// supportedReportSet: z
|
||||
// }
|
||||
prop: node => {
|
||||
return complex(node, {
|
||||
resourcetype: false,
|
||||
supportedCalendarComponentSet: false,
|
||||
supportedReportSet: false,
|
||||
currentUserPrincipal: false,
|
||||
groups: false,
|
||||
invite: false
|
||||
});
|
||||
},
|
||||
|
||||
resourcetype: node => {
|
||||
return childNodes(node).map(childNode => childNode.localName);
|
||||
},
|
||||
|
||||
groups: node => complex(node, { group: true }, 'group'),
|
||||
group: node => {
|
||||
return childNodes(node).map(childNode => childNode.nodeValue);
|
||||
},
|
||||
invite: node => complex(node, { user: true }, 'user'),
|
||||
user: node => complex(node, { href: false, access:false }),
|
||||
access: node => complex(node, {}),
|
||||
//access: node => {
|
||||
// return childNodes(node).map(childNode => childNode.localName);
|
||||
//},
|
||||
|
||||
// [x, y, z]
|
||||
supportedCalendarComponentSet: node => complex(node, { comp: true }, 'comp'),
|
||||
|
||||
// [x, y, z]
|
||||
supportedReportSet: node => {
|
||||
return complex(node, { supportedReport: true }, 'supportedReport');
|
||||
},
|
||||
|
||||
comp: node => node.getAttribute('name'),
|
||||
|
||||
// x
|
||||
supportedReport: node => complex(node, { report: false }, 'report'),
|
||||
|
||||
report: node => {
|
||||
return childNodes(node).map(childNode => childNode.localName);
|
||||
},
|
||||
|
||||
href: node => {
|
||||
return decodeURIComponent(childNodes(node)[0].nodeValue);
|
||||
},
|
||||
|
||||
currentUserPrincipal: node => {
|
||||
return complex(node, {href: false}, 'href');
|
||||
}
|
||||
};
|
||||
|
||||
function complex(node, childspec, collapse) {
|
||||
let result = {};
|
||||
for (let key in childspec) {
|
||||
if (childspec[key]) {
|
||||
// Create array since we're expecting multiple.
|
||||
result[key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
childNodes(node).forEach(
|
||||
childNode => traverseChild(node, childNode, childspec, result)
|
||||
);
|
||||
|
||||
return maybeCollapse(result, childspec, collapse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse child childNode of node with childspec and write outcome to result.
|
||||
*/
|
||||
function traverseChild(node, childNode, childspec, result) {
|
||||
if (childNode.nodeType === 3 && /^\s+$/.test(childNode.nodeValue)) {
|
||||
// Whitespace... nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
let localName = camelize(childNode.localName, '-');
|
||||
if (!(localName in childspec)) {
|
||||
debug('Unexpected node of type ' + localName + ' encountered while ' +
|
||||
'parsing ' + node.localName + ' node!');
|
||||
let value = childNode.textContent;
|
||||
if (localName in result) {
|
||||
if (!Array.isArray(result[camelCase])) {
|
||||
// Since we've already encountered this node type and we haven't yet
|
||||
// made an array for it, make an array now.
|
||||
result[localName] = [result[localName]];
|
||||
}
|
||||
|
||||
result[localName].push(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// First time we're encountering this node.
|
||||
result[localName] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
let traversal = traverse[localName](childNode);
|
||||
if (childspec[localName]) {
|
||||
// Expect multiple.
|
||||
result[localName].push(traversal);
|
||||
} else {
|
||||
// Expect single.
|
||||
result[localName] = traversal;
|
||||
}
|
||||
}
|
||||
|
||||
function maybeCollapse(result, childspec, collapse) {
|
||||
if (!collapse) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!childspec[collapse]) {
|
||||
return result[collapse];
|
||||
}
|
||||
|
||||
// Collapse array.
|
||||
return result[collapse].reduce((a, b) => a.concat(b), []);
|
||||
}
|
||||
|
||||
function childNodes(node) {
|
||||
let result = node.childNodes;
|
||||
if (!Array.isArray(result)) {
|
||||
result = Array.prototype.slice.call(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function children(node, localName) {
|
||||
return childNodes(node).filter(childNode => childNode.localName === localName);
|
||||
}
|
||||
|
||||
function child(node, localName) {
|
||||
return children(node, localName)[0];
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Polyfill from developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/find
|
||||
*/
|
||||
if (!Array.prototype.find) {
|
||||
Array.prototype.find = function(predicate) {
|
||||
if (this == null) {
|
||||
throw new TypeError('Array.prototype.find called on null or undefined');
|
||||
}
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = list.length >>> 0;
|
||||
var thisArg = arguments[1];
|
||||
var value;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
value = list[i];
|
||||
if (predicate.call(thisArg, value, i, list)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/**
|
||||
* Polyfill from developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
||||
*/
|
||||
if (!Object.assign) {
|
||||
Object.defineProperty(Object, 'assign', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(target, firstSource) {
|
||||
'use strict';
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert first argument to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var nextSource = arguments[i];
|
||||
if (nextSource === undefined || nextSource === null) {
|
||||
continue;
|
||||
}
|
||||
nextSource = Object(nextSource);
|
||||
|
||||
var keysArray = Object.keys(Object(nextSource));
|
||||
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
|
||||
var nextKey = keysArray[nextIndex];
|
||||
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
|
||||
if (desc !== undefined && desc.enumerable) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,564 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* https://raw.github.com/facebook/regenerator/master/LICENSE file. An
|
||||
* additional grant of patent rights can be found in the PATENTS file in
|
||||
* the same directory.
|
||||
*/
|
||||
|
||||
!(function(global) {
|
||||
"use strict";
|
||||
|
||||
var hasOwn = Object.prototype.hasOwnProperty;
|
||||
var undefined; // More compressible than void 0.
|
||||
var iteratorSymbol =
|
||||
typeof Symbol === "function" && Symbol.iterator || "@@iterator";
|
||||
|
||||
var inModule = typeof module === "object";
|
||||
var runtime = global.regeneratorRuntime;
|
||||
if (runtime) {
|
||||
if (inModule) {
|
||||
// If regeneratorRuntime is defined globally and we're in a module,
|
||||
// make the exports object identical to regeneratorRuntime.
|
||||
module.exports = runtime;
|
||||
}
|
||||
// Don't bother evaluating the rest of this file if the runtime was
|
||||
// already defined globally.
|
||||
return;
|
||||
}
|
||||
|
||||
// Define the runtime globally (as expected by generated code) as either
|
||||
// module.exports (if we're in a module) or a new, empty object.
|
||||
runtime = global.regeneratorRuntime = inModule ? module.exports : {};
|
||||
|
||||
function wrap(innerFn, outerFn, self, tryLocsList) {
|
||||
// If outerFn provided, then outerFn.prototype instanceof Generator.
|
||||
var generator = Object.create((outerFn || Generator).prototype);
|
||||
|
||||
generator._invoke = makeInvokeMethod(
|
||||
innerFn, self || null,
|
||||
new Context(tryLocsList || [])
|
||||
);
|
||||
|
||||
return generator;
|
||||
}
|
||||
runtime.wrap = wrap;
|
||||
|
||||
// Try/catch helper to minimize deoptimizations. Returns a completion
|
||||
// record like context.tryEntries[i].completion. This interface could
|
||||
// have been (and was previously) designed to take a closure to be
|
||||
// invoked without arguments, but in all the cases we care about we
|
||||
// already have an existing method we want to call, so there's no need
|
||||
// to create a new function object. We can even get away with assuming
|
||||
// the method takes exactly one argument, since that happens to be true
|
||||
// in every case, so we don't have to touch the arguments object. The
|
||||
// only additional allocation required is the completion record, which
|
||||
// has a stable shape and so hopefully should be cheap to allocate.
|
||||
function tryCatch(fn, obj, arg) {
|
||||
try {
|
||||
return { type: "normal", arg: fn.call(obj, arg) };
|
||||
} catch (err) {
|
||||
return { type: "throw", arg: err };
|
||||
}
|
||||
}
|
||||
|
||||
var GenStateSuspendedStart = "suspendedStart";
|
||||
var GenStateSuspendedYield = "suspendedYield";
|
||||
var GenStateExecuting = "executing";
|
||||
var GenStateCompleted = "completed";
|
||||
|
||||
// Returning this object from the innerFn has the same effect as
|
||||
// breaking out of the dispatch switch statement.
|
||||
var ContinueSentinel = {};
|
||||
|
||||
// Dummy constructor functions that we use as the .constructor and
|
||||
// .constructor.prototype properties for functions that return Generator
|
||||
// objects. For full spec compliance, you may wish to configure your
|
||||
// minifier not to mangle the names of these two functions.
|
||||
function Generator() {}
|
||||
function GeneratorFunction() {}
|
||||
function GeneratorFunctionPrototype() {}
|
||||
|
||||
var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype;
|
||||
GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
|
||||
GeneratorFunctionPrototype.constructor = GeneratorFunction;
|
||||
GeneratorFunction.displayName = "GeneratorFunction";
|
||||
|
||||
runtime.isGeneratorFunction = function(genFun) {
|
||||
var ctor = typeof genFun === "function" && genFun.constructor;
|
||||
return ctor
|
||||
? ctor === GeneratorFunction ||
|
||||
// For the native GeneratorFunction constructor, the best we can
|
||||
// do is to check its .name property.
|
||||
(ctor.displayName || ctor.name) === "GeneratorFunction"
|
||||
: false;
|
||||
};
|
||||
|
||||
runtime.mark = function(genFun) {
|
||||
genFun.__proto__ = GeneratorFunctionPrototype;
|
||||
genFun.prototype = Object.create(Gp);
|
||||
return genFun;
|
||||
};
|
||||
|
||||
runtime.async = function(innerFn, outerFn, self, tryLocsList) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var generator = wrap(innerFn, outerFn, self, tryLocsList);
|
||||
var callNext = step.bind(generator, "next");
|
||||
var callThrow = step.bind(generator, "throw");
|
||||
|
||||
function step(method, arg) {
|
||||
var record = tryCatch(generator[method], generator, arg);
|
||||
if (record.type === "throw") {
|
||||
reject(record.arg);
|
||||
return;
|
||||
}
|
||||
|
||||
var info = record.arg;
|
||||
if (info.done) {
|
||||
resolve(info.value);
|
||||
} else {
|
||||
Promise.resolve(info.value).then(callNext, callThrow);
|
||||
}
|
||||
}
|
||||
|
||||
callNext();
|
||||
});
|
||||
};
|
||||
|
||||
function makeInvokeMethod(innerFn, self, context) {
|
||||
var state = GenStateSuspendedStart;
|
||||
|
||||
return function invoke(method, arg) {
|
||||
if (state === GenStateExecuting) {
|
||||
throw new Error("Generator is already running");
|
||||
}
|
||||
|
||||
if (state === GenStateCompleted) {
|
||||
// Be forgiving, per 25.3.3.3.3 of the spec:
|
||||
// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
|
||||
return doneResult();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
var delegate = context.delegate;
|
||||
if (delegate) {
|
||||
if (method === "return" ||
|
||||
(method === "throw" && delegate.iterator[method] === undefined)) {
|
||||
// A return or throw (when the delegate iterator has no throw
|
||||
// method) always terminates the yield* loop.
|
||||
context.delegate = null;
|
||||
|
||||
// If the delegate iterator has a return method, give it a
|
||||
// chance to clean up.
|
||||
var returnMethod = delegate.iterator["return"];
|
||||
if (returnMethod) {
|
||||
var record = tryCatch(returnMethod, delegate.iterator, arg);
|
||||
if (record.type === "throw") {
|
||||
// If the return method threw an exception, let that
|
||||
// exception prevail over the original return or throw.
|
||||
method = "throw";
|
||||
arg = record.arg;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (method === "return") {
|
||||
// Continue with the outer return, now that the delegate
|
||||
// iterator has been terminated.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var record = tryCatch(
|
||||
delegate.iterator[method],
|
||||
delegate.iterator,
|
||||
arg
|
||||
);
|
||||
|
||||
if (record.type === "throw") {
|
||||
context.delegate = null;
|
||||
|
||||
// Like returning generator.throw(uncaught), but without the
|
||||
// overhead of an extra function call.
|
||||
method = "throw";
|
||||
arg = record.arg;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delegate generator ran and handled its own exceptions so
|
||||
// regardless of what the method was, we continue as if it is
|
||||
// "next" with an undefined arg.
|
||||
method = "next";
|
||||
arg = undefined;
|
||||
|
||||
var info = record.arg;
|
||||
if (info.done) {
|
||||
context[delegate.resultName] = info.value;
|
||||
context.next = delegate.nextLoc;
|
||||
} else {
|
||||
state = GenStateSuspendedYield;
|
||||
return info;
|
||||
}
|
||||
|
||||
context.delegate = null;
|
||||
}
|
||||
|
||||
if (method === "next") {
|
||||
if (state === GenStateSuspendedYield) {
|
||||
context.sent = arg;
|
||||
} else {
|
||||
delete context.sent;
|
||||
}
|
||||
|
||||
} else if (method === "throw") {
|
||||
if (state === GenStateSuspendedStart) {
|
||||
state = GenStateCompleted;
|
||||
throw arg;
|
||||
}
|
||||
|
||||
if (context.dispatchException(arg)) {
|
||||
// If the dispatched exception was caught by a catch block,
|
||||
// then let that catch block handle the exception normally.
|
||||
method = "next";
|
||||
arg = undefined;
|
||||
}
|
||||
|
||||
} else if (method === "return") {
|
||||
context.abrupt("return", arg);
|
||||
}
|
||||
|
||||
state = GenStateExecuting;
|
||||
|
||||
var record = tryCatch(innerFn, self, context);
|
||||
if (record.type === "normal") {
|
||||
// If an exception is thrown from innerFn, we leave state ===
|
||||
// GenStateExecuting and loop back for another invocation.
|
||||
state = context.done
|
||||
? GenStateCompleted
|
||||
: GenStateSuspendedYield;
|
||||
|
||||
var info = {
|
||||
value: record.arg,
|
||||
done: context.done
|
||||
};
|
||||
|
||||
if (record.arg === ContinueSentinel) {
|
||||
if (context.delegate && method === "next") {
|
||||
// Deliberately forget the last sent value so that we don't
|
||||
// accidentally pass it on to the delegate.
|
||||
arg = undefined;
|
||||
}
|
||||
} else {
|
||||
return info;
|
||||
}
|
||||
|
||||
} else if (record.type === "throw") {
|
||||
state = GenStateCompleted;
|
||||
// Dispatch the exception by looping back around to the
|
||||
// context.dispatchException(arg) call above.
|
||||
method = "throw";
|
||||
arg = record.arg;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function defineGeneratorMethod(method) {
|
||||
Gp[method] = function(arg) {
|
||||
return this._invoke(method, arg);
|
||||
};
|
||||
}
|
||||
defineGeneratorMethod("next");
|
||||
defineGeneratorMethod("throw");
|
||||
defineGeneratorMethod("return");
|
||||
|
||||
Gp[iteratorSymbol] = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
Gp.toString = function() {
|
||||
return "[object Generator]";
|
||||
};
|
||||
|
||||
function pushTryEntry(locs) {
|
||||
var entry = { tryLoc: locs[0] };
|
||||
|
||||
if (1 in locs) {
|
||||
entry.catchLoc = locs[1];
|
||||
}
|
||||
|
||||
if (2 in locs) {
|
||||
entry.finallyLoc = locs[2];
|
||||
entry.afterLoc = locs[3];
|
||||
}
|
||||
|
||||
this.tryEntries.push(entry);
|
||||
}
|
||||
|
||||
function resetTryEntry(entry) {
|
||||
var record = entry.completion || {};
|
||||
record.type = "normal";
|
||||
delete record.arg;
|
||||
entry.completion = record;
|
||||
}
|
||||
|
||||
function Context(tryLocsList) {
|
||||
// The root entry object (effectively a try statement without a catch
|
||||
// or a finally block) gives us a place to store values thrown from
|
||||
// locations where there is no enclosing try statement.
|
||||
this.tryEntries = [{ tryLoc: "root" }];
|
||||
tryLocsList.forEach(pushTryEntry, this);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
runtime.keys = function(object) {
|
||||
var keys = [];
|
||||
for (var key in object) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.reverse();
|
||||
|
||||
// Rather than returning an object with a next method, we keep
|
||||
// things simple and return the next function itself.
|
||||
return function next() {
|
||||
while (keys.length) {
|
||||
var key = keys.pop();
|
||||
if (key in object) {
|
||||
next.value = key;
|
||||
next.done = false;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
// To avoid creating an additional object, we just hang the .value
|
||||
// and .done properties off the next function object itself. This
|
||||
// also ensures that the minifier will not anonymize the function.
|
||||
next.done = true;
|
||||
return next;
|
||||
};
|
||||
};
|
||||
|
||||
function values(iterable) {
|
||||
if (iterable) {
|
||||
var iteratorMethod = iterable[iteratorSymbol];
|
||||
if (iteratorMethod) {
|
||||
return iteratorMethod.call(iterable);
|
||||
}
|
||||
|
||||
if (typeof iterable.next === "function") {
|
||||
return iterable;
|
||||
}
|
||||
|
||||
if (!isNaN(iterable.length)) {
|
||||
var i = -1, next = function next() {
|
||||
while (++i < iterable.length) {
|
||||
if (hasOwn.call(iterable, i)) {
|
||||
next.value = iterable[i];
|
||||
next.done = false;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
next.value = undefined;
|
||||
next.done = true;
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
return next.next = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Return an iterator with no values.
|
||||
return { next: doneResult };
|
||||
}
|
||||
runtime.values = values;
|
||||
|
||||
function doneResult() {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
|
||||
Context.prototype = {
|
||||
constructor: Context,
|
||||
|
||||
reset: function() {
|
||||
this.prev = 0;
|
||||
this.next = 0;
|
||||
this.sent = undefined;
|
||||
this.done = false;
|
||||
this.delegate = null;
|
||||
|
||||
this.tryEntries.forEach(resetTryEntry);
|
||||
|
||||
// Pre-initialize at least 20 temporary variables to enable hidden
|
||||
// class optimizations for simple generators.
|
||||
for (var tempIndex = 0, tempName;
|
||||
hasOwn.call(this, tempName = "t" + tempIndex) || tempIndex < 20;
|
||||
++tempIndex) {
|
||||
this[tempName] = null;
|
||||
}
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
this.done = true;
|
||||
|
||||
var rootEntry = this.tryEntries[0];
|
||||
var rootRecord = rootEntry.completion;
|
||||
if (rootRecord.type === "throw") {
|
||||
throw rootRecord.arg;
|
||||
}
|
||||
|
||||
return this.rval;
|
||||
},
|
||||
|
||||
dispatchException: function(exception) {
|
||||
if (this.done) {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
var context = this;
|
||||
function handle(loc, caught) {
|
||||
record.type = "throw";
|
||||
record.arg = exception;
|
||||
context.next = loc;
|
||||
return !!caught;
|
||||
}
|
||||
|
||||
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
||||
var entry = this.tryEntries[i];
|
||||
var record = entry.completion;
|
||||
|
||||
if (entry.tryLoc === "root") {
|
||||
// Exception thrown outside of any try block that could handle
|
||||
// it, so set the completion value of the entire function to
|
||||
// throw the exception.
|
||||
return handle("end");
|
||||
}
|
||||
|
||||
if (entry.tryLoc <= this.prev) {
|
||||
var hasCatch = hasOwn.call(entry, "catchLoc");
|
||||
var hasFinally = hasOwn.call(entry, "finallyLoc");
|
||||
|
||||
if (hasCatch && hasFinally) {
|
||||
if (this.prev < entry.catchLoc) {
|
||||
return handle(entry.catchLoc, true);
|
||||
} else if (this.prev < entry.finallyLoc) {
|
||||
return handle(entry.finallyLoc);
|
||||
}
|
||||
|
||||
} else if (hasCatch) {
|
||||
if (this.prev < entry.catchLoc) {
|
||||
return handle(entry.catchLoc, true);
|
||||
}
|
||||
|
||||
} else if (hasFinally) {
|
||||
if (this.prev < entry.finallyLoc) {
|
||||
return handle(entry.finallyLoc);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("try statement without catch or finally");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
abrupt: function(type, arg) {
|
||||
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
||||
var entry = this.tryEntries[i];
|
||||
if (entry.tryLoc <= this.prev &&
|
||||
hasOwn.call(entry, "finallyLoc") &&
|
||||
this.prev < entry.finallyLoc) {
|
||||
var finallyEntry = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finallyEntry &&
|
||||
(type === "break" ||
|
||||
type === "continue") &&
|
||||
finallyEntry.tryLoc <= arg &&
|
||||
arg <= finallyEntry.finallyLoc) {
|
||||
// Ignore the finally entry if control is not jumping to a
|
||||
// location outside the try/catch block.
|
||||
finallyEntry = null;
|
||||
}
|
||||
|
||||
var record = finallyEntry ? finallyEntry.completion : {};
|
||||
record.type = type;
|
||||
record.arg = arg;
|
||||
|
||||
if (finallyEntry) {
|
||||
this.next = finallyEntry.finallyLoc;
|
||||
} else {
|
||||
this.complete(record);
|
||||
}
|
||||
|
||||
return ContinueSentinel;
|
||||
},
|
||||
|
||||
complete: function(record, afterLoc) {
|
||||
if (record.type === "throw") {
|
||||
throw record.arg;
|
||||
}
|
||||
|
||||
if (record.type === "break" ||
|
||||
record.type === "continue") {
|
||||
this.next = record.arg;
|
||||
} else if (record.type === "return") {
|
||||
this.rval = record.arg;
|
||||
this.next = "end";
|
||||
} else if (record.type === "normal" && afterLoc) {
|
||||
this.next = afterLoc;
|
||||
}
|
||||
},
|
||||
|
||||
finish: function(finallyLoc) {
|
||||
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
||||
var entry = this.tryEntries[i];
|
||||
if (entry.finallyLoc === finallyLoc) {
|
||||
this.complete(entry.completion, entry.afterLoc);
|
||||
resetTryEntry(entry);
|
||||
return ContinueSentinel;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"catch": function(tryLoc) {
|
||||
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
||||
var entry = this.tryEntries[i];
|
||||
if (entry.tryLoc === tryLoc) {
|
||||
var record = entry.completion;
|
||||
if (record.type === "throw") {
|
||||
var thrown = record.arg;
|
||||
resetTryEntry(entry);
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
}
|
||||
|
||||
// The context.catch method must only be called with a location
|
||||
// argument that corresponds to a known catch block.
|
||||
throw new Error("illegal catch attempt");
|
||||
},
|
||||
|
||||
delegateYield: function(iterable, resultName, nextLoc) {
|
||||
this.delegate = {
|
||||
iterator: values(iterable),
|
||||
resultName: resultName,
|
||||
nextLoc: nextLoc
|
||||
};
|
||||
|
||||
return ContinueSentinel;
|
||||
}
|
||||
};
|
||||
})(
|
||||
// Among the various tricks for obtaining a reference to the global
|
||||
// object, this seems to be the most reliable technique that does not
|
||||
// use indirect eval (which violates Content Security Policy).
|
||||
typeof global === "object" ? global :
|
||||
typeof window === "object" ? window :
|
||||
typeof self === "object" ? self : this
|
||||
);
|
|
@ -1,244 +0,0 @@
|
|||
import { multistatus } from './parser';
|
||||
import * as template from './template';
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) depth - optional value for Depth header.
|
||||
* (Array.<Object>) props - list of props to request.
|
||||
*/
|
||||
export function addressBookQuery(options) {
|
||||
return collectionQuery(
|
||||
template.addressBookQuery({ props: options.props || [] }),
|
||||
{ depth: options.depth }
|
||||
);
|
||||
}
|
||||
|
||||
export function addressBookMultiget(options) {
|
||||
return collectionQuery(
|
||||
template.addressBookMultiget({ props: options.props || [], hrefs: options.hrefs || [] }),
|
||||
{ depth: options.depth }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) data - put request body.
|
||||
* (String) method - http method.
|
||||
* (String) etag - cached calendar object etag.
|
||||
*/
|
||||
export function basic(options) {
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: options.method,
|
||||
requestData: options.data,
|
||||
transformRequest: transformRequest
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) depth - optional value for Depth header.
|
||||
* (Array.<Object>) filters - list of filters to send with request.
|
||||
* (Array.<Object>) props - list of props to request.
|
||||
* (String) timezone - VTIMEZONE calendar object.
|
||||
*/
|
||||
export function calendarQuery(options) {
|
||||
return collectionQuery(
|
||||
template.calendarQuery({
|
||||
props: options.props || [],
|
||||
filters: options.filters || [],
|
||||
timezone: options.timezone
|
||||
}),
|
||||
{
|
||||
depth: options.depth
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function collectionQuery(requestData, options) {
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
function transformResponse(xhr) {
|
||||
return multistatus(xhr.responseText).response.map(res => {
|
||||
return { href: res.href, props: getProps(res.propstat) };
|
||||
});
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: 'REPORT',
|
||||
requestData: requestData,
|
||||
transformRequest: transformRequest,
|
||||
transformResponse: transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
export function mkcol(options) {
|
||||
let requestData = template.mkcol({ props: options.props });
|
||||
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: 'MKCOL',
|
||||
requestData: requestData,
|
||||
transformRequest: transformRequest
|
||||
});
|
||||
}
|
||||
|
||||
export function proppatch(options) {
|
||||
let requestData = template.proppatch({ props: options.props });
|
||||
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: 'PROPPATCH',
|
||||
requestData: requestData,
|
||||
transformRequest: transformRequest
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) depth - optional value for Depth header.
|
||||
* (Array.<Object>) props - list of props to request.
|
||||
*/
|
||||
export function propfind(options) {
|
||||
let requestData = template.propfind({ props: options.props });
|
||||
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
function transformResponse(xhr) {
|
||||
let responses = multistatus(xhr.responseText).response.map(res => {
|
||||
return { href: res.href, props: getProps(res.propstat) };
|
||||
});
|
||||
|
||||
if (!options.mergeResponses) {
|
||||
return responses;
|
||||
}
|
||||
|
||||
// Merge the props.
|
||||
let merged = mergeProps(responses.map(res => res.props));
|
||||
let hrefs = responses.map(res => res.href);
|
||||
return { props: merged, hrefs: hrefs };
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: 'PROPFIND',
|
||||
requestData: requestData,
|
||||
transformRequest: transformRequest,
|
||||
transformResponse: transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* (String) depth - option value for Depth header.
|
||||
* (Array.<Object>) props - list of props to request.
|
||||
* (Number) syncLevel - indicates scope of the sync report request.
|
||||
* (String) syncToken - synchronization token provided by the server.
|
||||
*/
|
||||
export function syncCollection(options) {
|
||||
let requestData = template.syncCollection({
|
||||
props: options.props,
|
||||
syncLevel: options.syncLevel,
|
||||
syncToken: options.syncToken
|
||||
});
|
||||
|
||||
function transformRequest(xhr) {
|
||||
setRequestHeaders(xhr, options);
|
||||
}
|
||||
|
||||
function transformResponse(xhr) {
|
||||
let object = multistatus(xhr.responseText);
|
||||
let responses = object.response.map(res => {
|
||||
return { href: res.href, props: getProps(res.propstat) };
|
||||
});
|
||||
|
||||
return { responses: responses, syncToken: object.syncToken };
|
||||
}
|
||||
|
||||
return new Request({
|
||||
method: 'REPORT',
|
||||
requestData: requestData,
|
||||
transformRequest: transformRequest,
|
||||
transformResponse: transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
export class Request {
|
||||
constructor(options={}) {
|
||||
Object.assign(this, {
|
||||
method: null,
|
||||
requestData: null,
|
||||
transformRequest: null,
|
||||
transformResponse: null,
|
||||
onerror: null
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
function getProp(propstat) {
|
||||
if (/404/g.test(propstat.status)) {
|
||||
return null;
|
||||
}
|
||||
if (/5\d{2}/g.test(propstat.status) ||
|
||||
/4\d{2}/g.test(propstat.status)) {
|
||||
throw new Error('Bad status on propstat: ' + propstat.status);
|
||||
}
|
||||
|
||||
return ('prop' in propstat) ? propstat.prop : null;
|
||||
}
|
||||
|
||||
export function mergeProps(props) {
|
||||
return props.reduce((a, b) => Object.assign(a, b), {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map propstats to props.
|
||||
*/
|
||||
export function getProps(propstats) {
|
||||
return mergeProps(
|
||||
propstats
|
||||
.map(getProp)
|
||||
.filter(prop => prop && typeof prop === 'object')
|
||||
);
|
||||
}
|
||||
|
||||
export function setRequestHeaders(request, options) {
|
||||
if ('contentType' in options) {
|
||||
request.setRequestHeader('Content-Type', options.contentType);
|
||||
} else {
|
||||
request.setRequestHeader('Content-Type', 'application/xml;charset=utf-8');
|
||||
}
|
||||
|
||||
if ('depth' in options) {
|
||||
request.setRequestHeader('Depth', options.depth);
|
||||
}
|
||||
|
||||
if ('etag' in options) {
|
||||
request.setRequestHeader('If-Match', options.etag);
|
||||
}
|
||||
|
||||
if ('destination' in options) {
|
||||
request.setRequestHeader('Destination', options.destination);
|
||||
}
|
||||
|
||||
if ('overwrite' in options) {
|
||||
request.setRequestHeader('Overwrite', options.overwrite);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* @fileoverview Group requests together and then abort as a group.
|
||||
*
|
||||
* var sandbox = new dav.Sandbox();
|
||||
* return Promise.all([
|
||||
* dav.createEvent(event, { sandbox: sandbox }),
|
||||
* dav.deleteEvent(other, { sandbox: sandbox })
|
||||
* ])
|
||||
* .catch(function() {
|
||||
* // Something went wrong so abort all requests.
|
||||
* sandbox.abort;
|
||||
* });
|
||||
*/
|
||||
let debug = require('./debug')('dav:sandbox');
|
||||
|
||||
export class Sandbox {
|
||||
constructor() {
|
||||
this.requestList = [];
|
||||
}
|
||||
|
||||
add(request) {
|
||||
debug('Adding request to sandbox.');
|
||||
this.requestList.push(request);
|
||||
}
|
||||
|
||||
abort() {
|
||||
debug('Aborting sandboxed requests.');
|
||||
this.requestList.forEach(request => request.abort());
|
||||
}
|
||||
}
|
||||
|
||||
export function createSandbox() {
|
||||
return new Sandbox();
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import prop from './prop';
|
||||
|
||||
function href(href) {
|
||||
return `<d:href>${href}</d:href>`;
|
||||
}
|
||||
|
||||
export default function addressBookMultiget(object) {
|
||||
return `<card:addressbook-multiget xmlns:card="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
${object.props.map(prop).join("")}
|
||||
</d:prop>
|
||||
${object.hrefs.map(href).join("")}
|
||||
</card:addressbook-multiget>`;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import prop from './prop';
|
||||
|
||||
export default function addressBookQuery(object) {
|
||||
return `<card:addressbook-query xmlns:card="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
${object.props.map(prop).join("")}
|
||||
</d:prop>
|
||||
<!-- According to http://stackoverflow.com/questions/23742568/google-carddav-api-addressbook-multiget-returns-400-bad-request,
|
||||
Google's CardDAV server requires a filter element. I don't think all addressbook-query calls need a filter in the spec though? -->
|
||||
</card:addressbook-query>`
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import filter from './filter';
|
||||
import prop from './prop';
|
||||
|
||||
export default function calendarQuery(object) {
|
||||
return `<c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav"
|
||||
xmlns:cs="http://calendarserver.org/ns/"
|
||||
xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
${object.props.map(prop)}
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
${object.filters.map(filter)}
|
||||
</c:filter>
|
||||
${object.timezone ? '<c:timezone>' + object.timezone + '</c:timezone>' : ''}
|
||||
</c:calendar-query>`;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
export default function filter(item) {
|
||||
if (!item.children || !item.children.length) {
|
||||
return `<c:${item.type} ${formatAttrs(item.attrs)}/>`;
|
||||
}
|
||||
|
||||
let children = item.children.map(filter);
|
||||
return `<c:${item.type} ${formatAttrs(item.attrs)}>
|
||||
${children}
|
||||
</c:${item.type}>`;
|
||||
}
|
||||
|
||||
function formatAttrs(attrs) {
|
||||
if (typeof attrs !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.keys(attrs)
|
||||
.map(attr => `${attr}="${attrs[attr]}"`)
|
||||
.join(' ');
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
exports.addressBookQuery = require('./address_book_query');
|
||||
exports.addressBookMultiget = require('./address_book_multiget');
|
||||
exports.calendarQuery = require('./calendar_query');
|
||||
exports.propfind = require('./propfind');
|
||||
exports.syncCollection = require('./sync_collection');
|
||||
exports.mkcol = require('./mkcol');
|
||||
exports.proppatch = require('./proppatch');
|
|
@ -1,14 +0,0 @@
|
|||
import prop from './prop';
|
||||
|
||||
export default function mkcol(object) {
|
||||
return `<d:mkcol xmlns:c="urn:ietf:params:xml:ns:caldav"
|
||||
xmlns:card="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:cs="http://calendarserver.org/ns/"
|
||||
xmlns:d="DAV:">
|
||||
<d:set>
|
||||
<d:prop>
|
||||
${object.props.map(prop)}
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</d:mkcol>`;
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import * as ns from '../namespace';
|
||||
|
||||
/**
|
||||
* @param {Object} filter looks like
|
||||
*
|
||||
* {
|
||||
* type: 'comp-filter',
|
||||
* attrs: {
|
||||
* name: 'VCALENDAR'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Or maybe
|
||||
*
|
||||
* {
|
||||
* type: 'time-range',
|
||||
* attrs: {
|
||||
* start: '20060104T000000Z',
|
||||
* end: '20060105T000000Z'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* You can nest them like so:
|
||||
*
|
||||
* {
|
||||
* type: 'comp-filter',
|
||||
* attrs: { name: 'VCALENDAR' },
|
||||
* children: [{
|
||||
* type: 'comp-filter',
|
||||
* attrs: { name: 'VEVENT' },
|
||||
* children: [{
|
||||
* type: 'time-range',
|
||||
* attrs: { start: '20060104T000000Z', end: '20060105T000000Z' }
|
||||
* }]
|
||||
* }]
|
||||
* }
|
||||
*/
|
||||
export default function prop(item) {
|
||||
var tagName = `${xmlnsPrefix(item.namespace)}:${item.name}`;
|
||||
var attrs = (item.attrs || []).map(makeAttr).join(' ');
|
||||
if (!item.children || !item.children.length) {
|
||||
if (typeof item.value === "undefined") {
|
||||
return `<${tagName} ${attrs}/>`;
|
||||
}
|
||||
return `<${tagName} ${attrs}>${item.value}</${tagName}>`;
|
||||
}
|
||||
|
||||
let children = item.children.map(prop);
|
||||
return `<${tagName} ${attrs}>
|
||||
${children.join('')}
|
||||
</${tagName}>`;
|
||||
}
|
||||
|
||||
function makeAttr(attr) {
|
||||
if (!attr.name) return '';
|
||||
if (!attr.value) return attr.name;
|
||||
return `${attr.name}="${attr.value}"`;
|
||||
}
|
||||
|
||||
function xmlnsPrefix(namespace) {
|
||||
switch (namespace) {
|
||||
case ns.DAV:
|
||||
return 'd';
|
||||
case ns.CALENDAR_SERVER:
|
||||
return 'cs';
|
||||
case ns.CALDAV:
|
||||
return 'c';
|
||||
case ns.CARDDAV:
|
||||
return 'card';
|
||||
case ns.OC:
|
||||
return 'oc';
|
||||
default:
|
||||
throw new Error('Unrecognized xmlns ' + namespace);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import prop from './prop';
|
||||
|
||||
export default function propfind(object) {
|
||||
return `<d:propfind xmlns:c="urn:ietf:params:xml:ns:caldav"
|
||||
xmlns:card="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:cs="http://calendarserver.org/ns/"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
${object.props.map(prop)}
|
||||
</d:prop>
|
||||
</d:propfind>`;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import prop from './prop';
|
||||
|
||||
export default function proppatch(object) {
|
||||
return `<d:propertyupdate xmlns:c="urn:ietf:params:xml:ns:caldav"
|
||||
xmlns:card="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:cs="http://calendarserver.org/ns/"
|
||||
xmlns:d="DAV:">
|
||||
<d:set>
|
||||
<d:prop>
|
||||
${object.props.map(prop)}
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</d:propertyupdate>`;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue