Vue cleanup and init

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2018-07-10 18:40:59 +02:00
parent 3f3ad0eeb8
commit 75f0d3c093
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
363 changed files with 14686 additions and 34137 deletions

12
.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"env",
{
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 11"]
}
}
]
]
}

View File

@ -1,3 +0,0 @@
{
"directory": "js/vendor"
}

View File

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

53
.eslintrc.js Normal file
View File

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

View File

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

22
.gitignore vendored
View File

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

1
.npmignore Normal file
View File

@ -0,0 +1 @@
node_modules

1
.prettierrc.js Normal file
View File

@ -0,0 +1 @@
module.exports = {};

View File

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

View File

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

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

View File

@ -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: **Were 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

View File

@ -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']
]
];

7
coverage/clover.xml Normal file
View File

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

View File

@ -0,0 +1 @@
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 */
}

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
@import 'details';
@import 'contactlist';
@import 'navigation';
@import 'settings';
@import 'animations';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
angular.module('contactsApp')
.controller('contactfilterCtrl', function() {
// eslint-disable-next-line no-unused-vars
var ctrl = this;
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
angular.module('contactsApp')
.controller('groupCtrl', function() {
// eslint-disable-next-line no-unused-vars
var ctrl = this;
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

8
js/dav/.gitignore vendored
View File

@ -1,8 +0,0 @@
SabreDAV/
/*.zip
/build/
/coverage/
/dav.js.map
/dav.min.js
/node_modules/

View File

@ -1,11 +0,0 @@
SabreDAV/
/*.zip
/coverage/
/Makefile
/.git
/.gitignore
/.jshintrc
/.travis.yml
/node_modules
/test

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
export default function debug(topic) {
return function(message) {
if (debug.enabled) {
console.log(`[${topic}] ${message}`);
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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